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.
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:
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
- The
- 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 justtrue
will
- See the
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++){}
, meaningn
is just stuck at 1 - On line 610, this
n
gets passed tog_strdup(argv[n])
, meaning it readsargv[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?
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 ourls
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:
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:
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.
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:
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.
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.
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.
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.
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.
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.
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…)