PwnKit: Lokale Privilegien-Eskalationsschwachstelle entdeckt

Von Bharat Jogi, Director, Vulnerabilty and Threat Research, Qualys Inc.

Das Qualys-Forschungsteam hat eine Schwachstelle in pkexec von polkit entdeckt. Polket ist ein SUID-Root-Programm, das standardmäßig auf jeder größeren Linux-Distribution installiert ist. Diese leicht auszunutzende Schwachstelle erlaubt es jedem unprivilegierten Benutzer, volle Root-Rechte auf einem verwundbaren Host zu erlangen, indem er diese Schwachstelle in der Standardkonfiguration ausnutzt.

Polkit (ehemals PolicyKit) ist eine Komponente zur Kontrolle systemweiter Privilegien in Unix-ähnlichen Betriebssystemen. Es bietet eine organisierte Methode für nicht-privilegierte Prozesse, mit privilegierten Prozessen zu kommunizieren. Es ist auch möglich, Polkit zu verwenden, um Befehle mit erhöhten Rechten auszuführen, indem der Befehl pkexec gefolgt von dem auszuführenden Befehl (mit Root-Rechten) verwendet wird.

Mögliche Auswirkungen der PwnKit-Schwachstelle

Wenn diese Sicherheitslücke erfolgreich ausgenutzt wird, kann jeder nicht privilegierte Benutzer Root-Rechte auf dem anfälligen Host erlangen. Die Sicherheitsforscher von Qualys konnten die Schwachstelle unabhängig verifizieren, einen Exploit entwickeln und volle Root-Rechte auf Standardinstallationen von Ubuntu, Debian, Fedora und CentOS erlangen. Andere Linux-Distributionen sind möglicherweise auch anfällig und können wahrscheinlich ausgenutzt werden. Diese Sicherheitslücke ist seit über 12 Jahren bekannt und betrifft alle Versionen von pkexec seit der ersten Version im Mai 2009 (Commit c8c3d83, “Add a pkexec(1) command”).

Sobald das Forschungsteam die Schwachstelle bestätigt hatte, wurde diese offengelegt und sowohl mit den Herstellern als auch den Open-Source-Distributionen koordiniert, um die Schwachstelle zu veröffentlichen.

Wie die PwnKit-Schwachstelle funktioniert

Der Beginn der main()-Funktion von pkexec verarbeitet die Befehlszeilenargumente (Zeilen 534-568) und sucht das auszuführende Programm, falls dessen Pfad nicht absolut ist, in den Verzeichnissen der Umgebungsvariablen PATH (Zeilen 610–640):

————————————————————————

435 main (int argc, char *argv[])

436 {

534 for (n = 1; n < (guint) argc; n++)

535 {

568 }

610 Pfad = g_strdup (argv[n]);

629 if (pfad[0] != ‘/’)

630 {

632 s = g_find_program_in_path (path);

639 argv[n] = path = s;

640 }

————————————————————————

Wenn die Anzahl der Kommandozeilenargumente argc gleich 0 ist – was bedeutet, dass die Argumentliste argv, die wir an execve() übergeben, leer ist, d.h. {NULL} – dann ist argv[0] leider NULL. Dies ist der Terminator der Argumentliste. Daher wird

in Zeile 534 die ganze Zahl n dauerhaft auf 1 gesetzt;

in Zeile 610 der Zeiger path außerhalb der Grenzen von argv[1] gelesen;

in Zeile 639 der Zeiger s außerhalb der Grenzen nach argv[1] geschrieben.

Aber was genau wird aus argv[1] gelesen und in argv[1] geschrieben, das außerhalb der Grenzen liegt?

Um diese Frage zu beantworten, müssen wir kurz abschweifen. Wenn wir ein neues Programm mit execve() ausführen, kopiert der Kernel unsere Argumente, Umgebungszeichenketten 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

“Programm” “-Option” NULL “Wert” “PATH=Name” NULL

Da die argv- und envp-Zeiger im Speicher zusammenhängend sind, ist argv[1], wenn argc gleich 0 ist, in Wirklichkeit envp[0], der Zeiger auf unsere erste Umgebungsvariable “value”, der außerhalb der Grenzen liegt. Daraus folgt:

In Zeile 610 wird der Pfad des auszuführenden Programms aus argv[1] (d.h. envp[0]) ausgelesen und zeigt auf “value”;

In Zeile 632 wird dieser Pfad “value” an g_find_program_in_path() übergeben (weil “value” nicht mit einem Schrägstrich beginnt, wie in Zeile 629);

Dann sucht g_find_program_in_path() nach einer ausführbaren Datei mit dem Namen “value” in den Verzeichnissen unserer Umgebungsvariablen PATH;

Wenn eine solche ausführbare Datei gefunden wird, wird ihr vollständiger Pfad an die main()-Funktion von pkexec zurückgegeben (in Zeile 632);

In Zeile 639 schließlich wird dieser vollständige Pfad außerplanmäßig in argv[1] (d. h. envp[0]) geschrieben, wodurch unsere erste Umgebungsvariable überschrieben wird.

Genauer formuliert:

Wenn unsere PATH-Umgebungsvariable “PATH=name” lautet und das Verzeichnis “name” (im aktuellen Arbeitsverzeichnis) existiert und eine ausführbare Datei mit dem Namen “value” enthält, dann wird ein Zeiger auf die Zeichenkette “name/value” out-of-bounds in envp[0] geschrieben;

ODER

Wenn unser PATH “PATH=name=.” ist und wenn das Verzeichnis “name=.” existiert und eine ausführbare Datei mit dem Namen “value” enthält, dann wird ein Zeiger auf die Zeichenkette “name=./value” außerhalb des zulässigen Bereichs in envp[0] geschrieben.

Mit anderen Worten, dieser Out-of-Bounds-Schreibvorgang ermöglicht es uns, eine „unsichere“ Umgebungsvariable (z. B. LD_PRELOAD) wieder in die Umgebung von pkexec einzuführen. Diese „unsicheren“ Variablen werden normalerweise (durch ld.so) aus der Umgebung von SUID-Programmen entfernt, bevor die main()-Funktion aufgerufen wird. Wir werden dieses mächtige Primitiv im folgenden Abschnitt ausnutzen.

Anmerkung in letzter Minute: polkit unterstützt auch Nicht-Linux-Betriebssysteme wie Solaris und *BSD, aber wir haben deren Ausnutzbarkeit nicht untersucht. Wir stellen jedoch fest, dass OpenBSD nicht ausnutzbar ist, da sein Kernel sich weigert, ein Programm auszuführen, wenn argc gleich 0 ist.

Da die Angriffsfläche für diese Schwachstelle sowohl unter Linux als auch unter anderen Betriebssystemen sehr groß ist, wird empfohlen, dass Benutzer sofort Patches für diese Schwachstelle installieren.