regreSSHion (CVE-2024-6387) un anno e mezzo dopo: anatomia tecnica, lezioni dalla patching delle PMI e hardening SSH che applico oggi
La sera del 1 luglio 2024, alle 19:47 ora italiana, ho ricevuto contemporaneamente quattro alert da clienti diversi sullo stesso topic. Il post di disclosure di Qualys su regreSSHion era appena uscito: una RCE pre-auth in OpenSSH server, CVSS 9.8 come da entry NIST NVD, affecting tutte le versioni portable da 8.5p1 a 9.7p1, ovvero la quasi totalità dei sistemi Linux moderni. Quella sera ho passato sei ore al telefono e in SSH a coordinare la patching di emergenza su circa 200 server in produzione di vari clienti - un mix di Hetzner, OVH, Aruba e qualche bare-metal in colocation. Ne ho ricavato due cose: una stanchezza degna di un nuovo anno, e un set di lezioni operative che hanno cambiato come configuro SSH oggi sui clienti.
Un anno e mezzo dopo, regreSSHion non è più una vulnerabilità "calda" - la patch è disponibile da 18 mesi, OpenSSH 9.8p1+ è ovunque, e a oggi nessuno ha pubblicato un exploit pubblico funzionante per architetture 64-bit (e ne riparliamo più avanti perché il dettaglio è interessante). Ma il valore di questo bug come case study tecnico resta intatto: è un esempio quasi didattico di come una regressione introdotta da un refactor banale possa riaprire una vulnerabilità chiusa 18 anni prima, e di come la combinazione "race condition + signal handler async-unsafe + memory corruption" sia uno dei pattern più sottovalutati nelle codebase scritte in C. Ne parliamo da consulente che ha vissuto il giorno del fix, non da chi ha letto l'articolo dopo.
Anatomia tecnica: un signal handler che chiama syslog() in contesto async-signal
Il bug ha un cuore tecnico semplicissimo, ed è proprio questa semplicità che lo rende istruttivo. OpenSSH sshd ha un parametro di configurazione chiamato LoginGraceTime (default 120 secondi) che definisce il timeout entro cui un client deve completare l'handshake di autenticazione. Quando un client supera quel timeout, il kernel invia un segnale SIGALRM al processo sshd figlio che gestisce quella connessione. sshd ha installato un signal handler - grace_alarm_handler() - che viene invocato dal kernel proprio per gestire questo evento.
Il problema è cosa fa quel signal handler. Da POSIX 1003.1 §2.4.3, un signal handler può chiamare in modo sicuro solo le cosiddette async-signal-safe functions - un elenco molto ristretto che include cose come _exit(), signal(), write() su file descriptor noti, e poco altro. Non include malloc(), free(), syslog(), printf(), e tutta la libreria stdio. Il motivo: queste funzioni mantengono stato interno protetto da lock o tabelle globali, e se vengono interrotte da un segnale e re-invocate dal signal handler in un contesto async, possono lasciare strutture dati corrotte (un classico esempio di reentrancy hazard).
grace_alarm_handler() in OpenSSH 8.5p1–9.7p1 chiamava - direttamente o transitivamente - syslog() per loggare l'evento di timeout, e indirettamente chiamava malloc() e free() attraverso le funzioni di pulizia. Se SIGALRM arrivava esattamente mentre il processo principale era dentro una chiamata a malloc() (probabilità non trascurabile, perché il flusso di autenticazione SSH alloca/libera memoria continuamente per parsing dei pacchetti), il signal handler interrompeva l'allocazione a metà, riprendeva l'esecuzione di malloc() dal signal handler stesso, e lasciava la heap del processo in uno stato inconsistente. A quel punto, con tecniche di heap grooming e heap spraying mirate, un attaccante remoto può manipolare le strutture interne dell'allocatore glibc (ptmalloc2) per ottenere overlapping chunks, sovrascrivere puntatori a funzione e infine ottenere arbitrary code execution come root - perché il processo sshd figlio in fase di autenticazione gira ancora con privilegi root prima del privilege drop.
Il vero "trucco" tecnico di regreSSHion non è la race condition in sé. È il fatto che la finestra è ampia (120 secondi di default), il numero di tentativi non è limitato, e l'attaccante controlla il timing perché può deciderlo lui (smettendo di rispondere all'handshake al momento giusto). Questa è la differenza fra una vulnerabilità teorica e una realmente sfruttabile.
Perché il bug è tornato dopo 18 anni: la storia del refactor di ottobre 2020
Questa è la parte che ogni sviluppatore dovrebbe leggere e ricordare. La stessa identica vulnerabilità - signal handler che chiama funzioni async-unsafe in grace_alarm_handler - era stata scoperta nel 2006 e tracciata come CVE-2006-5051. La fix originale inseriva una guard sigsafe = 1 che evitava il path pericoloso quando il codice era in un contesto signal handler, e funzionava correttamente. Il bug era chiuso e dimenticato.
Nel mese di ottobre 2020, il commit di refactor di OpenSSH che introduceva il supporto a OpenSSH 8.5p1 ha rimosso quel guard come parte di una pulizia generale del codice di logging. L'autore del commit non sapeva (o non si è ricordato) del motivo per cui quel guard esisteva: era un commento difensivo aggiunto 14 anni prima per una vulnerabilità ormai dimenticata, che a uno sguardo superficiale sembrava codice morto. Tolto il guard, regreSSHion è tornata silenziosamente, e nessuno se ne è accorto per quasi quattro anni.
Le lezioni sono tre, e le tatuerei sui developer junior se potessi:
- Ogni guard difensivo nel codice deve avere un commento che spiega perché esiste, non cosa fa. "// signal-safe path" è inutile; "// signal-safe path: senza questo, CVE-2006-5051 si riapre" è la differenza fra mantenere il fix e rimuoverlo per cleanliness.
- I refactor "estetici" su codice di sicurezza sono pericolosi quanto le nuove feature. Un PR che "semplifica il logging" su un signal handler dovrebbe richiedere lo stesso livello di code review di un PR che cambia l'autenticazione.
- I test di regressione di sicurezza spariscono nel tempo se non sono inclusi nella suite ufficiale. CVE-2006-5051 non aveva un test specifico nella suite OpenSSH che andasse a verificare la signal-safety di
grace_alarm_handler. Senza quel test, non c'è guard automatico contro la regressione.
Questo schema - fix-corretto-rimosso-da-refactor-successivo - è incredibilmente comune nelle codebase legacy che ho audited. Sul tema più ampio di come modernizzare codice critico senza perdere fix di sicurezza ho scritto la mia guida pratica al refactoring di codice PHP legacy, dove uno dei punti chiave è proprio "documenta il perché di ogni guard difensivo, sempre".
Quanto era davvero sfruttabile: 32-bit vs 64-bit, ASLR e 10.000 tentativi
Qui c'è una sfumatura tecnica che la stampa generalista ha quasi sempre saltato. Qualys, nel post di disclosure, è stata onesta: il PoC funzionante è stato sviluppato su sistemi Linux x86 32-bit, e il tempo medio per ottenere root con un exploit working era di 6-8 ore. Sui sistemi x86_64 (64-bit) - che sono praticamente la totalità dei server in produzione moderni - Qualys ha dichiarato di stare lavorando al port ma di non avere ancora un exploit pubblico funzionante al momento del disclosure. A 18 mesi di distanza, a oggi nessun exploit pubblico per regreSSHion su x86_64 è stato rilasciato, e quanto so dai canali di security research, neanche Qualys ha condiviso pubblicamente il loro lavoro post-disclosure. Lo confermano report aggiornati come il write-up di Offsec sull'exploit chain di regreSSHion.
Questo non significa "non era un bug serio". Significa che c'era una finestra di disponibilità asimmetrica: un attaccante state-sponsored con risorse poteva probabilmente svilupparlo, un attaccante opportunista no. La differenza pratica nel triage è enorme: per i clienti PMI con server pubblici, la priorità della patch era altissima ma non "emergency immediato", e per i clienti con SSH dietro VPN o bastion la priorità era media. Questo tipo di nuance va capito perché i media (e qualche security vendor) hanno descritto regreSSHion come "il nuovo Heartbleed" - non lo era, perché Heartbleed era exploitabile dappertutto al primo tentativo, regreSSHion richiedeva 10.000 tentativi mediamente e su 64-bit era tutto da dimostrare.
Va anche detto che il MaxStartups di default in OpenSSH è 10:30:100 (start dropping connessioni dopo 10 in fase di handshake, full drop dopo 100), e questo limita naturalmente il rate di tentativi. Con LoginGraceTime 120 e MaxStartups 100, il numero massimo di tentativi/120s è ~100, che porta i 10.000 tentativi necessari a circa 200 minuti di traffico anomalo verso lo stesso server. In quel tempo, qualunque sistema di monitoring decente o qualunque fail2ban configurato avrebbe rilevato e bloccato l'attaccante. Non un colpo di mitragliatrice - più un assedio lungo con probabilità di successo non garantita.
C'è poi il caso "cugino": CVE-2024-6409, scoperto da Solar Designer pochi giorni dopo, durante la review post-disclosure su Red Hat Enterprise Linux 9. Affligge il privsep child process di OpenSSH 8.7p1/8.8p1 patchato downstream da Red Hat - ovvero il processo già unprivileged, non il parent root. CVSS 7.0, RCE non in root, e nessun exploit pubblico mai rilasciato. La differenza chiave operativa: la mitigation LoginGraceTime 0 fixa CVE-2024-6387 ma non mitiga del tutto CVE-2024-6409. Va patchato a livello pacchetto. Su questo Red Hat ha avuto qualche giorno di confusione perché aveva già integrato il fix per 6387 prima che 6409 fosse pubblico, e Solar Designer ha pubblicamente espresso rammarico per il coordination delay.
Cosa ha funzionato (e cosa no) nella patching delle PMI a luglio 2024
La sera del 1 luglio 2024 ho lavorato fino alle 2 del mattino su circa 200 server in produzione. I clienti erano un mix di Debian 11/12, Ubuntu 22.04 LTS, qualche AlmaLinux 9 e due Rocky Linux 9. Il pattern operativo è stato sempre lo stesso, perché era l'unico sicuro: identificare la versione vulnerabile con ssh -V (gli 8.5p1–9.7p1 sono dentro), applicare la mitigation LoginGraceTime 0 come fix di emergenza valido per CVE-2024-6387 (ma non per CVE-2024-6409), verificare sempre l'effetto reale con sshd -T invece di fidarsi del file, e infine patchare il pacchetto appena la repo della distro pubblicava la fix.
ssh -V 2>&1
sudo sed -i.bak 's/^#\?LoginGraceTime.*/LoginGraceTime 0/' /etc/ssh/sshd_config
sudo sshd -t && sudo systemctl reload sshd
sudo sshd -T | grep -i logingracetime
sudo apt update && sudo apt install --only-upgrade openssh-server
sudo systemctl restart sshd && ssh -VTre lezioni dalla nottata:
Cosa ha funzionato. Avere un inventario aggiornato dei server. I tre clienti che avevano un Ansible inventory pulito sono stati patchati in 30 minuti con un singolo playbook che applicava la mitigation LoginGraceTime 0, validava la configurazione con sshd -t, ricaricava il servizio e verificava lo stato. Gli altri li ho dovuti fare a mano, server per server, con tutto il rischio di inconsistenze e dimenticanze associato.
Cosa NON ha funzionato. Tre server avevano sshd_config.d/ con drop-in file che sovrascrivevano LoginGraceTime con il default. La mia sed iniziale modificava il file principale ma il drop-in vinceva, e per 20 minuti ho creduto di aver patchato server che invece erano ancora vulnerabili. Lezione: dopo qualunque modifica a sshd_config, valida sempre con sshd -T | grep <option> invece di fidarti del file. Da allora questo è uno standard operativo.
Cosa avrei dovuto fare prima. Avere SSH dietro un bastion/jump host o (meglio) dietro WireGuard/Tailscale. I sei clienti che già avevano questo setup sono stati declassificati immediatamente da "patch entro stanotte" a "patch durante la finestra di manutenzione standard di domani mattina", perché la loro superficie di attacco internet-facing per SSH era zero. Quei sei clienti hanno dormito quella notte. Gli altri 194 no. È diventato il mio standard di consulenza per ogni nuovo progetto: SSH non si espone mai direttamente su internet pubblico, anche se "tanto ho fail2ban e password strong". Per la transizione operativa di un'infrastruttura PMI verso questa postura, vedi anche la mia guida alla migrazione sicura di VPS unmanaged con zero downtime.
Hardening SSH oltre la patch: cinque regole che ora applico sempre
Dopo regreSSHion ho consolidato un baseline di hardening SSH che applico a tutti i nuovi progetti PMI e a tutti gli audit di legacy infrastructure. Non è esoterico - è la combinazione delle pratiche standard NIST/CIS più alcune lezioni operative che ho imparato a costo di nottate come quella del 1 luglio.
Ecco l'estratto delle direttive critiche che applico in /etc/ssh/sshd_config:
Port 22
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
AllowUsers deploy maurizio # whitelist esplicita, mai "tutti"
LoginGraceTime 30 # ridotto da 120, abbastanza per chi usa key manager
MaxAuthTries 3
MaxSessions 5
MaxStartups 10:30:60 # rate limit aggressivo sull'handshake
ClientAliveInterval 300
ClientAliveCountMax 2
AllowTcpForwarding no # disabilita se non serve port forwarding
X11Forwarding no
PermitTunnel noLe cinque regole che applico in ordine di importanza:
- SSH non si espone direttamente su internet. Sempre dietro WireGuard, Tailscale, o un bastion host con MFA. Questa è la regola che azzera l'esposizione a vulnerabilità future che ancora non conosciamo. Costo di setup: 30 minuti su un VPS dedicato. Ritorno: dormire la notte.
- Solo autenticazione a chiave pubblica, mai password.
PasswordAuthentication noè non negoziabile. Le chiavi sono Ed25519 o RSA 4096+. Le chiavi di servizio vivono in~/.ssh/authorized_keysconfrom="ip"restriction quando possibile. - Whitelist utenti esplicita con
AllowUsers. Mai lasciare il default che permette login a qualunque utente locale. Su un server compromesso a livello applicativo, questo è la differenza fra "movimento laterale via account di servizio" e "muro". - fail2ban con jail SSH e ban list di IP geografici noti per attacchi. Il jail standard di fail2ban su Debian/Ubuntu è già un ottimo baseline; l'ho tarato a 5 tentativi falliti / 10 minuti / ban di 24h. Per un cliente PMI italiano con clientela italiana, blocco preventivo di /8 da paesi con flusso di traffico anomalo verso SSH.
- systemd hardening sull'unit
sshd.servicequando supportato.NoNewPrivileges=yes,ProtectSystem=strict,ProtectHome=read-only,PrivateTmp=yes. Non blocca exploit di pre-auth (perché sshd ha bisogno di forkare con root), ma riduce significativamente l'impatto post-exploitation.
L'integrazione di queste pratiche con il resto della postura di sicurezza di un server PMI è coperta nella mia checklist di hardening urgente per Debian e Laravel e, sul versante più ampio della compliance, nella mia guida alla Direttiva NIS2 per server Hetzner/OVH. Il punto operativo è uno: SSH esposto su internet pubblico è un debito tecnico in attesa del prossimo CVE.
regreSSHion non è stato il primo bug critico in OpenSSH e non sarà l'ultimo. La vera lezione di questa vicenda non è "patcha velocemente" - quella è ovvia. È che la postura difensiva degli stack che gestisci deve essere progettata per resistere alla vulnerabilità successiva, quella che ancora non conosciamo. Chi aveva SSH dietro WireGuard la sera del 1 luglio 2024 ha guardato il caos da fuori; chi aveva SSH dritto su porta 22 su internet pubblico ha passato la notte sui server. La differenza non è stata una decisione di sicurezza presa quel giorno: è stata una decisione di architettura presa mesi prima. Se la tua infrastruttura SSH è ancora esposta direttamente su internet, o se non hai mai fatto un audit serio dell'sshd_config dei tuoi server di produzione, scopri il mio approccio alla sicurezza infrastrutturale per PMI - dieci anni di operations su VPS unmanaged Hetzner/OVH/Aruba mi hanno insegnato a riconoscere quali difese reggono una nottata da CVE 9.8 e quali si sbriciolano. Se vuoi una valutazione concreta della tua postura SSH attuale e un piano di hardening operativo, contattami per una consulenza: in due settimane di lavoro ti consegno audit dei sshd_config, setup WireGuard/bastion, integrazione fail2ban e hardening systemd, con runbook documentato per il tuo team.