
Questo è un PoC che ho scritto per CVE-2024-6387
Qualys Security Advisory
regreSSHion: RCE nel server OpenSSH, su sistemi Linux basati su glibc (CVE-2024-6387)
Sommario SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, del 2005)
Basta un atto di fede
-- The Interrupters, "Leap of Faith"
Nota preliminare: OpenSSH è uno dei software più sicuri al mondo; questa vulnerabilità è un passo falso in un'implementazione altrimenti quasi impeccabile. Il suo design e codice con difesa in profondità sono un modello e un'ispirazione, e ringraziamo gli sviluppatori di OpenSSH per il loro lavoro esemplare.
Abbiamo scoperto una vulnerabilità (una race condition del gestore di segnali) nel server OpenSSH (sshd): se un client non si autentica entro LoginGraceTime secondi (120 di default, 600 nelle vecchie versioni di OpenSSH), il gestore SIGALRM di sshd viene chiamato in modo asincrono, ma questo gestore di segnali chiama varie funzioni che non sono async-signal-safe (ad esempio, syslog()). Questa race condition colpisce sshd nella sua configurazione predefinita.
Dopo aver indagato, abbiamo realizzato che questa vulnerabilità è in realtà una regressione di CVE-2006-5051 ("Race condition del gestore di segnali in OpenSSH precedente alla 4.4 permette a un attaccante remoto di causare un denial of service (crash), ed eventualmente eseguire codice arbitrario"), riportata nel 2006 da Mark Dowd.
Questa regressione è stata introdotta nell'ottobre 2020 (OpenSSH 8.5p1) dal commit 752250c ("revised log infrastructure for OpenSSH"), che ha accidentalmente rimosso un "#ifdef DO_LOG_SAFE_IN_SIGHAND" da sigdie(), una funzione chiamata direttamente dal gestore SIGALRM di sshd. In altre parole:
OpenSSH < 4.4p1 è vulnerabile a questa race condition del gestore di segnali, se non è stato backportato con la patch per CVE-2006-5051, o non è stato patchato contro CVE-2008-4109, che era una correzione errata per CVE-2006-5051;
4.4p1 <= OpenSSH < 8.5p1 non è vulnerabile a questa race condition del gestore di segnali (perché l' "#ifdef DO_LOG_SAFE_IN_SIGHAND" aggiunto a sigdie() dalla patch per CVE-2006-5051 ha trasformato questa funzione non sicura in una chiamata sicura _exit(1));
8.5p1 <= OpenSSH < 9.8p1 è di nuovo vulnerabile a questa race condition del gestore di segnali (perché l' "#ifdef DO_LOG_SAFE_IN_SIGHAND" è stato accidentalmente rimosso da sigdie()).
Questa vulnerabilità è sfruttabile da remoto su sistemi Linux basati su glibc, dove syslog() stesso chiama funzioni async-signal-unsafe (ad esempio, malloc() e free()): una esecuzione di codice remoto non autenticata come root, perché colpisce il codice privilegiato di sshd, che non è in sandbox ed esegue con pieni privilegi. Non abbiamo investigato nessun altro libc o sistema operativo; ma OpenBSD non è notoriamente vulnerabile, perché il suo gestore SIGALRM chiama syslog_r(), una versione di syslog() più async-signal-safer, inventata da OpenBSD nel 2001.
Per sfruttare questa vulnerabilità da remoto (per quanto ne sappiamo, CVE-2006-5051 non è mai stata sfruttata con successo prima), ci siamo ispirati a un lavoro visionario, "Delivering Signals for Fun and Profit", pubblicato nel 2001 da Michal Zalewski:
https://lcamtuf.coredump.cx/signals.txt
Tuttavia, ci siamo immediatamente trovati di fronte a tre problemi principali:
Da un punto di vista teorico, dobbiamo trovare un percorso di codice utile che, se interrotto al momento giusto da SIGALRM, lasci sshd in uno stato incoerente, e dobbiamo quindi sfruttare questo stato incoerente all'interno del gestore SIGALRM.
Da un punto di vista pratico, dobbiamo trovare un modo per raggiungere questo percorso di codice utile in sshd, e massimizzare le nostre possibilità di interromperlo al momento giusto.
Da un punto di vista temporale, dobbiamo trovare un modo per aumentare ulteriormente le nostre possibilità di interrompere questo percorso di codice utile al momento giusto, da remoto.
Per concentrarci su questi tre problemi senza dover combattere subito contro tutte le moderne protezioni del sistema operativo (in particolare, ASLR e NX), abbiamo deciso di sfruttare prima le vecchie versioni di OpenSSH, su i386, e poi, basandoci su questa esperienza, le versioni recenti:
Prima, "SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3", da "debian-30r6-dvd-i386-binary-1_NONUS.iso": questa è la prima versione Debian che ha la separazione dei privilegi abilitata per default e che è patchata contro tutte le vulnerabilità critiche di quell'epoca (in particolare, CVE-2003-0693 e CVE-2002-0640).
Per sfruttare questa versione da remoto, interrompiamo una chiamata a free() con SIGALRM (all'interno del codice di parsing delle chiavi pubbliche di sshd), lasciamo l'heap in uno stato incoerente, e sfruttiamo questo stato incoerente durante un'altra chiamata a free(), all'interno del gestore SIGALRM.
Nei nostri esperimenti, ci vogliono in media ~10.000 tentativi per vincere questa race condition; cioè, con 10 connessioni (MaxStartups) accettate ogni 600 secondi (LoginGraceTime), ci vuole in media ~1 settimana per ottenere una shell remota come root.
Secondo, "SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3", da "ubuntu-6.06.1-server-i386.iso": questa è l'ultima versione Ubuntu ancora vulnerabile a CVE-2006-5051 ("Race condition del gestore di segnali in OpenSSH precedente alla 4.4").
Per sfruttare questa versione da remoto, interrompiamo una chiamata a pam_start() con SIGALRM, lasciamo una delle strutture di PAM in uno stato incoerente, e sfruttiamo questo stato incoerente durante una chiamata a pam_end(), all'interno del gestore SIGALRM.
Nei nostri esperimenti, ci vogliono in media ~10.000 tentativi per vincere questa race condition; cioè, con 10 connessioni (MaxStartups) accettate ogni 120 secondi (LoginGraceTime), ci vuole in media ~1-2 giorni per ottenere una shell remota come root.
Infine, "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2", da "debian-12.5.0-i386-DVD-1.iso": questa è l'attuale versione stabile Debian, ed è vulnerabile alla regressione di CVE-2006-5051.
Per sfruttare questa versione da remoto, interrompiamo una chiamata a malloc() con SIGALRM (all'interno del codice di parsing delle chiavi pubbliche di sshd), lasciamo l'heap in uno stato incoerente, e sfruttiamo questo stato incoerente durante un'altra chiamata a malloc(), all'interno del gestore SIGALRM (più precisamente, dentro syslog()).
Nei nostri esperimenti, ci vogliono in media ~10.000 tentativi per vincere questa race condition, quindi ~3-4 ore con 100 connessioni (MaxStartups) accettate ogni 120 secondi (LoginGraceTime). In ultima analisi, ci vuole in media ~6-8 ore per ottenere una shell remota come root, perché possiamo indovinare correttamente l'indirizzo di glibc solo la metà delle volte (a causa di ASLR).
Questa ricerca è ancora in corso:
abbiamo preso di mira solo macchine virtuali, non server bare-metal, su un collegamento di rete per lo più stabile (~10ms di jitter dei pacchetti);
siamo convinti che vari aspetti dei nostri exploit possano essere notevolmente migliorati;
abbiamo iniziato a lavorare a un exploit per amd64, che è molto più difficile a causa del ASLR più forte.