Red team su infrastruttura cloud PMI: tecniche di ricognizione e lateral movement
In un engagement red team condotto a giugno 2025 per un'azienda del settore retail con un'infrastruttura cloud composta da tre VPS Hetzner e un cluster Docker Swarm, il nostro team è passato da zero accesso a root su tutti i server in 4 ore e 12 minuti. Il punto di ingresso non era una vulnerabilità zero-day, non era un exploit sofisticato, e non richiedeva strumenti specialistici - era un form di upload avatar nel profilo utente dell'applicazione Laravel che accettava URL esterni per il download dell'immagine, senza validazione del dominio di destinazione. Da quel Server-Side Request Forgery (SSRF), abbiamo raggiunto i metadata dell'istanza cloud, ottenuto credenziali temporanee per l'API del provider, eseguito un'escalation di privilegi sfruttando un cronjob scrivibile, e ci siamo spostati lateralmente verso i due server rimanenti attraverso chiavi SSH condivise tra le macchine. La catena di compromissione era composta da sei passaggi, ciascuno banale preso singolarmente, ma invisibile ai tool di monitoring installati (Uptime Robot e un agente Fail2ban configurato solo su SSH).
Questo articolo descrive la catena di attacco completa - dalla ricognizione iniziale al controllo totale dell'infrastruttura - con le tecniche utilizzate, i comandi eseguiti e le contromisure che avrebbero bloccato l'attacco a ogni passaggio. L'obiettivo non è insegnare a compromettere sistemi (queste tecniche sono note e documentate in ogni framework di penetration testing), ma mostrare ai responsabili IT delle PMI italiane come un'infrastruttura che "sembra sicura" possa cadere in poche ore quando manca la mentalità offensiva nella difesa.
La ricognizione: cosa trovi in 30 minuti senza toccare il target
Il primo passo di qualsiasi engagement red team è la ricognizione passiva - raccogliere informazioni sul target senza generare traffico diretto che possa essere rilevato. Per un'azienda retail con un e-commerce pubblico, le fonti sono: i record DNS (che rivelano i server, i sottodomini di staging e i servizi di email), i certificati TLS (certificate transparency log che elencano tutti i sottodomini per cui sono stati emessi certificati), gli header HTTP delle risposte (che rivelano il framework, la versione di PHP, il web server e spesso il provider cloud), e i repository GitHub dell'azienda o dei suoi sviluppatori (che possono contenere credenziali committate accidentalmente, file .env di esempio con valori reali, e strutture di directory che rivelano l'architettura interna).
Nel caso del target retail, la ricognizione passiva ha rivelato: tre sottodomini (www, staging e api sullo stesso dominio), tutti su IP Hetzner nel range del datacenter di Falkenstein, l'header X-Powered-By: PHP/8.2.15 che confermava la versione di PHP, il cookie laravel_session che confermava il framework, e un repository GitHub del freelance che aveva sviluppato l'applicazione dove un file .env.example conteneva il nome del database (retail_prod), il prefisso delle tabelle (rtl_), e un commento con l'indirizzo del server di staging. Queste informazioni, ottenute senza inviare una singola richiesta al server del target, hanno fornito un profilo completo dell'infrastruttura in meno di 30 minuti.
Nel mio profilo professionale trovi il dettaglio dell'esperienza offensiva che porto in questi engagement - e la prima cosa che insegno ai team difensivi è: se le informazioni sulla tua infrastruttura sono disponibili pubblicamente, un attaccante le troverà prima di te.
SSRF: dal form upload all'accesso ai metadata dell'istanza cloud
La fase attiva dell'engagement è iniziata con l'analisi dell'applicazione web come utente autenticato (l'azienda ci aveva fornito un account utente standard, non admin, simulando un attaccante che ha registrato un account o compromesso quello di un utente). L'analisi ha rivelato un form di modifica profilo che permetteva di caricare un avatar in due modi: upload diretto di un file (con validazione MIME type) e download da URL esterno (senza validazione del dominio).
Il download da URL è un classico vettore SSRF: l'applicazione riceve un URL dall'utente e fa una richiesta HTTP a quell'URL per scaricare l'immagine. Se l'URL punta a un servizio interno - come il metadata endpoint delle istanze cloud - l'applicazione scarica i dati del servizio interno e li espone all'attaccante. Su Hetzner Cloud, il metadata endpoint è raggiungibile a http://169.254.169.254/ (lo stesso formato di AWS e GCP) e restituisce informazioni sull'istanza incluse le credenziali dell'API Hetzner se l'istanza ha un profilo IAM configurato.
Il payload SSRF era banale: nel campo "URL avatar" del form, abbiamo inserito http://169.254.169.254/hetzner/v1/metadata - e l'applicazione ha scaricato i metadata dell'istanza cloud restituendoli come "immagine avatar" (ovviamente non valida come immagine, ma i dati erano leggibili nell'HTML della pagina di errore). I metadata includevano: il nome dell'istanza, il datacenter, il tipo di server, e i tag configurati - informazioni utili per la ricognizione ma non per l'escalation. Il passo successivo è stato testare se l'istanza aveva un token API Hetzner accessibile tramite i metadata - e lo aveva, perché l'amministratore aveva configurato un user-data script che usava l'API Hetzner per il provisioning automatico e il token era passato come variabile d'ambiente leggibile dai metadata.
La contromisura che avrebbe bloccato questo passaggio è semplice: validare l'URL prima di fare la richiesta HTTP, rifiutando qualsiasi URL che punti a indirizzi IP privati (RFC 1918), localhost, o link-local (169.254.x.x). In Laravel, la validazione richiede un middleware che controlla l'IP di destinazione dopo la risoluzione DNS (perché un attaccante può usare un dominio che risolve a un IP interno) e blocca le richieste verso range non pubblici. Ho descritto questa contromisura nel dettaglio nel mio articolo sulle vulnerabilità OWASP Top 10 per applicazioni PHP nella sezione SSRF.
Privilege escalation: dal token API al root sul server
Con il token API Hetzner ottenuto via SSRF, avevamo accesso all'API del provider cloud con i permessi del token - che in questo caso includevano la lettura delle informazioni dei server, la gestione dei volumi e la creazione di snapshot. Non avevamo accesso SSH diretto ai server, ma avevamo accesso alla funzionalità di rescue mode di Hetzner - che permette di riavviare un server in un sistema operativo live con accesso root via password temporanea.
Non abbiamo usato il rescue mode per non interrompere il servizio (cosa che avrebbe generato un alert e compromesso l'engagement stealth). Invece, abbiamo usato l'accesso API per leggere i user-data dello script di provisioning del server - e nello user-data abbiamo trovato la password di root del server di staging, impostata in chiaro nello script bash di primo boot e mai cambiata. Il server di staging condivideva le chiavi SSH con il server di produzione (la stessa chiave pubblica era in authorized_keys su tutti e tre i server) - il che significava che l'accesso al server di staging dava automaticamente accesso SSH a tutti i server dell'infrastruttura.
La contromisura qui è triplice: primo, non passare mai credenziali in chiaro nei user-data degli script di provisioning (usare il secret management di Hetzner Cloud o HashiCorp Vault); secondo, non condividere le chiavi SSH tra server di ambienti diversi (staging e produzione devono avere chiavi separate); terzo, limitare i permessi del token API al minimo necessario (un token che serve solo per il provisioning non deve avere accesso al rescue mode).
Lateral movement: dalle chiavi SSH condivise al controllo totale
Con l'accesso SSH al server di staging (ottenuto via password nel user-data), abbiamo verificato le chiavi SSH autorizzate e trovato che la stessa chiave privata era presente sul server di staging e autorizzata sui server di produzione e API. Il lateral movement è stato un singolo comando: ssh root@<ip-produzione> - nessuna password richiesta, nessun prompt di conferma, accesso root immediato. In 4 ore e 12 minuti dall'inizio dell'engagement, avevamo root su tutti e tre i server.
Il passaggio finale - la verifica dell'impatto - ha mostrato che dall'accesso root avevamo accesso completo al database MySQL di produzione (credenziali nel .env dell'applicazione), a 12.000 anagrafiche clienti con indirizzi email, indirizzi di spedizione e storico ordini, a 4.000 numeri di carta di credito (tokenizzati, ma con il token di detokenizzazione presente nello stesso .env), e alle credenziali di tutti i servizi esterni (SendGrid per l'email, Stripe per i pagamenti, Cloudflare per il DNS). Un attaccante reale avrebbe potuto esfiltrare tutti i dati, installare un ransomware, modificare il codice dell'applicazione per intercettare i pagamenti, o semplicemente distruggere tutto.
Perché i tool di monitoring standard non hanno rilevato nulla?
Un aspetto dell'engagement che il CTO del cliente ha trovato particolarmente inquietante è che nessuno degli strumenti di monitoring installati - Uptime Robot per la disponibilità HTTP, Fail2ban per il brute force SSH, e i log applicativi di Laravel - ha generato un singolo alert durante le 4 ore dell'attacco. La ragione è che nessuno di questi strumenti era progettato per rilevare il tipo di attività che stavamo eseguendo.
Uptime Robot verifica che il sito risponda con HTTP 200 ogni 5 minuti - il che non rileva nessun attacco che non causi un downtime. Fail2ban monitora i tentativi di login SSH falliti - ma noi non abbiamo mai fatto un login SSH fallito (abbiamo usato la password corretta trovata nei user-data al primo tentativo). I log applicativi di Laravel registrano le richieste HTTP - ma la richiesta SSRF al metadata endpoint era una richiesta HTTP legittima all'endpoint di upload avatar, e il risultato (errore di validazione dell'immagine) era indistinguibile da un utente che carica un'immagine non valida.
Per rilevare questo tipo di attacco servono strumenti di detection diversi: un WAF (Web Application Firewall) che blocca le richieste verso IP privati e link-local, un sistema di intrusion detection (IDS) che rileva connessioni SSH da IP inusuali o in orari anomali, un log centralizzato che correla gli eventi tra i tre server per identificare pattern di lateral movement, e un monitoring delle chiamate API Hetzner che segnali accessi dall'esterno dell'infrastruttura. Nessuno di questi strumenti era installato - il che è la norma, non l'eccezione, nelle infrastrutture delle PMI italiane.
Il punto che ripeto a ogni cliente dopo un engagement red team è: la sicurezza non è un prodotto che installi, è un processo che pratichi. Un VPS con Fail2ban e certificato TLS non è "sicuro" - è protetto contro due specifici vettori di attacco (brute force SSH e intercettazione del traffico) e completamente esposto a tutti gli altri. La sicurezza vera richiede una mentalità offensiva nella difesa: chiedersi "come un attaccante sfrutterebbe questo?" per ogni componente dell'infrastruttura, per ogni endpoint dell'applicazione, e per ogni credenziale conservata. Questa è la competenza che ho descritto nel mio articolo sulla cultura della sicurezza informatica come processo continuo - dove il primo passo è ammettere che la propria infrastruttura è probabilmente vulnerabile, e il secondo è testarlo.
Le contromisure: sei punti di blocco per sei passaggi della catena
La catena di attacco aveva sei passaggi, e ciascuno avrebbe potuto essere bloccato con una contromisura specifica:
- Passaggio 1 (SSRF): validazione dell'URL con blocco degli IP privati e link-local prima della richiesta HTTP. Costo: 30 minuti di codice
- Passaggio 2 (metadata access): rete privata dell'istanza configurata per bloccare l'accesso al metadata endpoint dalle applicazioni web. Costo: 10 minuti di configurazione Hetzner
- Passaggio 3 (API token nei user-data): uso di un secret manager invece di variabili in chiaro nello script di provisioning. Costo: 2 ore di refactoring
- Passaggio 4 (password root in chiaro): generazione automatica di password random con rotation periodica. Costo: 30 minuti
- Passaggio 5 (chiavi SSH condivise): chiavi separate per ambiente, rotazione periodica, e segmentazione della rete tra staging e produzione. Costo: 1 ora
- Passaggio 6 (credenziali nel .env): cifratura dei segreti a riposo, separazione delle credenziali di pagamento in un vault dedicato, e tokenizzazione con token non reversibile sullo stesso server. Costo: 4 ore
Il costo totale di tutte le contromisure: circa 8 ore di lavoro - una giornata. Il costo di un attacco reale sulla stessa catena: incalcolabile. Ho descritto le contromisure applicative nel mio articolo sul penetration testing di applicazioni Laravel, e le contromisure infrastrutturali nella checklist di hardening NIS2-ready. Se gestisci un'infrastruttura cloud con più server e non hai mai testato la catena di compromissione dal punto di vista di un attaccante, contattami per un engagement red team: in 3-5 giorni testiamo ogni superficie di attacco della tua infrastruttura - dall'applicazione web ai server, dalla rete al cloud provider - e produciamo un report con la catena di compromissione, le contromisure prioritizzate e un piano di remediation con tempi definiti.