
Ausführlicher Bericht und Proof-of-Concept-Exploit für CVE-2021-4034 (PolKit pkexec lokale Privilegieneskalation), einschließlich einer Docker-Lab-Umgebung für praktische Analyse und Debugging.
[toc]
Schwachstellennummer: CVE-2021-4034
Schwachstellenbewertung:
Schwachstellenprodukt: Linux PolKit (pkexec)
Betroffene Versionen: Betrifft Versionen von 2009 bis heute (aktuell 0.105) Referenz: http://its.dlut.edu.cn/info/1054/78309.htm
Ausnutzungsbedingungen: Linux lokaler Zugriff; pkexec ist eine suid-Datei und hat Ausführungsberechtigung
Quellcode-Bezug: apt source policykit-1
oder https://launchpad.net/ubuntu/bionic/+package/policykit-1
Docker-Umgebung: chenaotian/cve-2021-4034
Ich habe selbst einen Docker erstellt, der Folgendes bereitstellt:
pkexec mit Quellcode-DebuggingAlles befindet sich im Verzeichnis /root/:

su test kann man zum Test-Benutzer wechseln und es ausführen.Docker starten:
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
Exp testen:
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
Das betroffene Produkt ist der Befehl pkexec von Polkit. pkexec ist ähnlich wie sudo ein Werkzeug, mit dem wir Befehle als anderer Benutzer (normalerweise root) ausführen können. Mit dem Befehl dpkg kann man das Paket anzeigen, zu dem pkexec gehört:
dpkg -S /usr/bin/pkexec

Dann das Quellpaket beziehen (auch in meinem Docker enthalten) und anschließend aus dem Quellpaket selbst eine debug-fähige Version kompilieren, um das Debugging zu erleichtern.
Das Prinzip der Schwachstellenauslösung ist sehr einfach.
/polkit-0.105/src/programs/pkexec.c : 386 main
int
main (int argc, char *argv[])
{
··· ···
··· ···
/* Das bedeutet: Durchlaufe die Benutzereingabeparameter und setze Werte basierend auf den verschiedenen eingegebenen Parametern.
* Das Problem ist jedoch, dass die Schleife bei 1 beginnt und den Fall nicht berücksichtigt, dass der Benutzer keine Parameter eingegeben hat.
*/
for (n = 1; n < (guint) argc; n++)
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
··· ···
else //Wenn ein unbekannter Parameter erkannt wird, die Schleife verlassen, was bedeutet, dass dieser Parameter der auszuführende Befehl ist
{
break;
}
}
··· ···
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]); //Hole die konkrete Zeichenkette des auszuführenden Befehls
if (path == NULL)
{
···
}
if (path[0] != '/')
{
/* g_find_program_in_path() ist nicht angreifbar durch Angriffe über die Umgebung */
//Diese Funktion sucht nach dem absoluten Pfad des auszuführenden Befehls basierend auf der PATH-Umgebungsvariable.
s = g_find_program_in_path (path);
if (s == NULL)
{
···
}
g_free (path);
argv[n] = path = s;//Setze den erhaltenen absoluten Pfad zurück in den Befehlszeilenparameter
}
··· ···
··· ···
Basierend auf meinen Kommentaren im Code ergibt sich folgende Analyse:
pkexec ausgeführt werden soll).-- beginnt, wird dieser als der auszuführende Befehl angesehen, und die Schleife wird verlassen, um mit der folgenden Logik fortzufahren.g_find_program_in_path wird aufgerufen, um den absoluten Pfad des Befehls zu suchen. g_find_program_in_path sucht basierend auf der PATH-Umgebungsvariable nach dem absoluten Pfad des übergebenen Parameters (Befehl). Zum Beispiel gibt die Eingabe von cat den Wert /bin/cat zurück.Das ist gut zu verstehen, aber das Problem liegt darin:
Wenn ein Linux-Binärprogramm ausgeführt wird, werden die Befehlszeilenparameter argv[] und die Umgebungsvariablen environ[] am unteren Ende des Stacks abgelegt, und argv[] und environ[] sind miteinander verbunden. Das letzte Element von argv[] ist null.

Wenn pkexec über die Befehlszeile gestartet wird und keine anderen Parameter übergeben werden, ist argv[0] gleich "pkexec" und argv[1] ist \x00 – kein Problem. Wenn pkexec jedoch mit der Funktion execve gestartet wird und keine weiteren Parameter übergeben werden, ist argv[0] gleich \x00 und argv[1] zeigt auf die Umgebungsvariablen! Wenn dann argv[1] gelesen wird, wird über die Grenzen auf environ[0] zugegriffen.
Bei direktem Start von pkexec über die Befehlszeile ist argc gleich 1, argv[0] ist der Pfad von pkexec:

Beim Start von pkexec mit der Funktion execve ist argc gleich 0:

Welche Auswirkungen hat das? Wenn execve ohne weitere Parameter verwendet wird, hat argv[] die Länge 0, und somit ist argv[1] gleich environ[0]. Dadurch wird die oben analysierte Logik zu: Hole den Wert der ersten Umgebungsvariable und suche im PATH-Umgebungsvariablen nach dessen absolutem Pfad. Falls gefunden, schreibe ihn zurück in die erste Umgebungsvariable. Die Ausnutzungsmethode ergibt sich wie folgt:
Zuerst muss klar sein, dass pkexec eine privilegierte (suid) Datei ist:

Wie kann man in einer privilegierten Datei mit Hilfe von Umgebungsvariablen etwas anstellen? Zunächst ein kleines Detail:
Der dynamische Linker von Linux, ld-linux-x86-64.so.2, löscht beim Ausführen eines privilegierten Programms sensible Umgebungsvariablen:
Funktion _dl_non_dynamic_init: glibc-2.27/elf/dl-support.c : 307
void
_dl_non_dynamic_init (void)
{
··· ···
··· ···
if (__libc_enable_secure) //Im privilegierten Modus
{
static const char unsecure_envvars[] =
UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
EXTRA_UNSECURE_ENVVARS
#endif
;
const char *cp = unsecure_envvars;