// 1 ZERO-DAY · 1 CVE · 1 EXPLOIT NELLE ULTIME 24H

Forensic della Violazione di ShopBox: Un Caso di Studio su un Rilevamento Mancato

Devo raccontarvi della settimana peggiore della mia vita professionale. Non per compassione — perché capire come ho fallito nel rilevare questa cosa nel nostro ambiente di laboratorio potrebbe impedirvi di commettere gli stessi errori quando conta.

È successo diciotto mesi fa, quando ShopBox girava ancora su una singola istanza MySQL su 192.0.2.20 con quello che pensavo fosse un hardening adeguato. Mi sbagliavo. L'attaccante — un red team che avevamo assunto e poi essenzialmente dimenticato dopo la fine del contratto — ha trovato una breccia che avevo lasciato spalancata. Quello che segue è la mia ricostruzione, costruita dai frammenti che abbiamo recuperato.


La Scoperta: Prestazioni, Non Allarmi

Non l'abbiamo trovato perché il nostro WAF (Web Application Firewall, un filtro che ispeziona le richieste HTTP) ha urlato. L'abbiamo trovato perché Maria al NOC ha notato che il rapporto di hit della query cache su 192.0.2.20 era sceso dall'87% al 12% durante un weekend. Il database lavorava di più. Molto di più.

Ho estratto prima il slow query log. È lì che l'ho visto: lo stesso pattern SELECT * FROM products WHERE name LIKE '%...%', ma con queste mostruose clausole UNION appese, in esecuzione centinaia di volte all'ora. Qualcuno era stato nella nostra casella di ricerca per giorni.

Ecco la cronologia che ho ricostruito, con timestamp in UTC:

Timestamp Evento Fonte
2023-04-14T09:23:17Z Ricognizione iniziale: ' OR '1'='1 bloccato dalla regola WAF SQLI-001 Log di accesso WAF (parziale)
2023-04-14T11:45:33Z Passaggio a payload codificato in Unicode: %u0027%u0020%u004F%u0052%u0020%u0031%u003D%u0031 Gap del proxy — nessun log
2023-04-14T14:08:19Z Injection riuscita tramite parametro di ricerca; primo UNION SELECT per enumerare le colonne General log MySQL (rotato, recuperato)
2023-04-15T03:17:44Z Enumerazione dello schema completata; information_schema.tables interrogato General log MySQL
2023-04-15T08:52:11Z Inizia l'estrazione dati: tabella users, blocchi di 50 righe General log MySQL
2023-04-16T19:33:27Z L'estrazione si sposta sulla tabella payment_methods General log MySQL
2023-04-17T06:14:58Z Anomalia di prestazioni del database segnalata dal monitoraggio NOC Ticket interno #INC-2023-0442

Tre giorni. Quasi settantadue ore di estrazione continua. E sapevo solo delle prime due ore di tentativi falliti perché il WAF aveva loggato quelli.


I Reperiti Forensi: Cos'è Sopravvissuto, Cosa Non

Voglio mostrarvi esattamente con cosa abbiamo dovuto lavorare, perché i vuoti sono altrettanto istruttivi quanto i dati.

Recuperato: General Log MySQL Rotato

Il general log si era rotato tre volte durante l'incidente. Abbiamo recuperato la seconda rotazione dai nastri di backup — la prima era già scaduta, e la terza era ancora attiva. Ecco il frammento che mostrava il passaggio dalla ricognizione all'estrazione. Ho redatto i nomi effettivi delle tabelle con [REDACTED]:

# Recuperato da /var/lib/mysql/shopbox-general.log.2.gz
# Hash: sha256:a3f7c...e2d9 (riferimento di verifica) 2023-04-14T14:08:19.004321Z 42 Query SELECT * FROM products WHERE name LIKE '%'
UNION SELECT null,null,null,null,null,null FROM information_schema.tables-- -%'
2023-04-14T14:08:19.447892Z 42 Query SELECT * FROM products WHERE name LIKE '%'
UNION SELECT table_name,null,null,null,null,null FROM information_schema.tables
WHERE table_schema='shopbox_production' LIMIT 50 OFFSET 0-- -%'
2023-04-14T14:12:55.883104Z 42 Query SELECT * FROM products WHERE name LIKE '%'
UNION SELECT column_name,null,null,null,null,null FROM information_schema.columns
WHERE table_name='[REDACTED]'-- -%'

Quel 42 è l'ID della connessione. Stessa sessione, stesso IP sorgente (l'indirizzo interno del load balancer, ovviamente), in esecuzione per ore. L'attaccante era paziente — incrementi di offset di 50, mai abbastanza avido da innescare le nostre soglie di conteggio righe.

Perso: Log Applicativi

I log applicativi di ShopBox su 192.0.2.10 erano stati troncati. Non in modo malevolo — semplicemente rotati aggressivamente perché non avevamo dimensionato correttamente la partizione. Avevamo quarantotto ore di log applicativi su un sistema che era stato compromesso per settantadue. I valori del parametro di ricerca, che avrebbero mostrato i payload grezzi prima di qualsiasi codifica, erano spariti.

Questo è un errore classico: loggare al layer sbagliato. Stavamo catturando a livello applicativo, ma il database era dove venivano effettivamente eseguite le query malevole. Come trattato in precedenza in questa guida, l'architettura di rilevamento necessita profondità. Avevo profondità in teoria. In pratica, avevo un layer con una finestra di quarantotto ore.

Il Gap del Proxy: Cecità del Termination TLS

È qui che ho davvero sbagliato. La nostra architettura aveva TLS che terminava all'AWS Network Load Balancer — quello che alcuni chiamano SSL offloading, dove la crittografia termina al load balancer così i server backend non bruciano CPU sulla negoziazione della sessione. Il traffico tra il load balancer e 192.0.2.10 era HTTP non crittografato.

I log di accesso del load balancer andavano su S3, come descrive la documentazione AWS. Ma il WAF — un appliance separato che avevamo deployato dietro il load balancer — vedeva solo il traffico HTTP già decodificato. Il payload codificato in Unicode %u0027%u0020%u004F%u0052... arrivava al WAF come i caratteri letterali ' OR '1'='1, ma a quel punto, il matching delle firme del WAF era già stato eseguito sulla forma codificata a livello di load balancer.

Aspetta — questo è sbagliato. Sia preciso: il WAF vedeva effettivamente la forma decodificata. Il problema era che la nostra regola SQLI-001 era scritta per fare match sulla stringa ASCII ' OR '1'='1 con asserzioni sui confini delle parole. L'Unicode decodificato arrivava con rappresentazioni di byte diverse che non corrispondevano alle ipotesi della firma sulla codifica dei caratteri. La regola si aspettava %27 o un apostrofo diretto; non teneva conto del fatto che %u0027 venisse decodificato allo stesso carattere visivo ma con byte diversi in memoria.

In termini semplici: Il WAF stava cercando un pattern specifico di byte che significava "tentativo di SQL injection". L'attaccante ha inviato lo stesso significato ma vestito in una codifica diversa, come scrivere una parola proibita in un alfabeto diverso che appare identico. Gli occhi del WAF erano addestrati per un solo alfabeto.

Ho verificato questo successivamente in laboratorio. La regola bloccava ' OR '1'='1 ogni volta. Non si è mai attivata su %u0027%u0020%u004F%u0052%u0020%u0031%u003D%u0031 nemmeno una volta.


Causa Radice: Il Deploy Parziale

Devo spiegare come questa fosse colpa mia, specificamente.

Sei mesi prima dell'incidente, avevo rafforzato il form di login di ShopBox. Ricordo il ticket: #SEC-112, "Parametrizzare tutte le query di autenticazione." Ho convertito login.php per usare prepared statement — dove la struttura della query SQL è fissa e l'input dell'utente è legato come dato, non concatenato nella stringa. L'ho testato. Sono passato oltre.

La funzione di ricerca in search.php? Stesso codebase. Stessi pattern. Ho guardato il codice. Ho persino scritto una nota: "Refactor a query parametrizzata — bassa priorità, nessun bypass diretto dell'autenticazione." Poi sono stato tirato in un altro progetto. La nota è rimasta nel backlog.

L'attaccante l'ha trovata nella terza ora della sua ricognizione. Il login era pietra. La ricerca era uno zerbino di benvenuto.

Questo è ciò che il Code Review Guide di OWASP avverte: la revisione del codice sicuro deve essere sistematica, non mirata in modo eroico. Avevo rivisto le parti spaventose e assunto che il resto andasse bene. La metodologia che avrei dovuto seguire — integrare la revisione nel SDLC con checklist che coprissero ogni query che consuma parametri URL — l'avrebbe rilevato. Non l'ho seguita. Ero un professionista senior che pensava di sapere dove fossero i rischi.


La Stima dell'Exfiltrazione

Dal general log recuperato, potevo contare le query UNION SELECT e la loro progressione LIMIT/OFFSET. L'attaccante ha estratto:

  • tabella users: ~4.200 righe in 84 query (blocchi di 50 righe)
  • tabella payment_methods: ~1.100 righe in 22 query
  • tabella orders: ~8.900 righe in 178 query (abbandonata a metà, probabilmente aveva ottenuto ciò che gli serviva)

Exfiltrazione totale stimata: approssimativamente 14.200 record in 72 ore. Il query log mostrava tempistica consistente — circa una query di estrazione ogni 4-7 minuti durante le ore di veglia, con gap più lunghi durante la notte. L'attaccante evitava il rilevamento basato sulla frequenza essendo noioso.


Post-Incidente: Cosa Ho Fatto Effettivamente

Non vi darò qui la checklist completa di remediation — quella è trattata più avanti in questa guida, nel capitolo "Defense in Depth". Ma voglio mostrarvi i risultati della code review, perché illustrano come l'hardening parziale crei un falso senso di sicurezza.

Ho eseguito un grep sull'intero codebase di ShopBox per la concatenazione di stringhe in SQL:

# Esegui da /var/www/shopbox su 192.0.2.10
# Variante più sicura: esegui su una copia del codebase, non su produzione grep -rn "SELECT.*\\$" --include="*.php" . > /tmp/potential-concatenation.txt
# output illustrativo — verifica sul tuo target
# ./search.php:47: $query = "SELECT * FROM products WHERE name LIKE '%" . $_GET['q'] . "%'";
# ./legacy/reporting.php:112: $sql = "SELECT * FROM orders WHERE date > '" . $start . "'";
# ./admin/export.php:203: $cmd = "SELECT * FROM " . $_POST['table']; # oh no

Tre hit. Tre ulteriori punti di injection. All'attaccante ne bastava uno.

⚠️ Solo per uso autorizzato e difensivo. Questo pattern grep è per la code review di sistemi di tua proprietà o per cui hai esplicito permesso di valutare. Non eseguirlo mai su sistemi di terze parti.

Il reperto in export.php era particolarmente brutto — i nomi di tabella dinamici non possono essere parametrizzati nella maggior parte delle API SQL, quindi quel pattern necessita un ridisegno architetturale, non solo un wrapper di prepared statement.


Cosa Ho Imparato: Gap di Rilevamento che Contano

Voglio lasciarvi con le lezioni specifiche, conquistate nel modo più duro:

Primo: Le regole WAF senza normalizzazione della codifica sono incomplete. Ora eseguiamo tutti i payload attraverso la canonicalizzazione prima del matching delle firme. Il WAF ancora non rileverà tutto, ma non mancherà i trucchi di codifica ovvi.

Secondo: Logga al layer dove l'attacco si esegue. I log applicativi sono per il debugging. I log del database sono per la forensics. Dimensionateli appropriatamente — la mia rotazione di quarantotto ore era una scelta che avevo fatto per risparmiare spazio su disco. Il costo è stato tre giorni di exfiltrazione alla cieca.

Terzo: L'hardening parziale è peggio dell'assenza di hardening, psicologicamente. Pensavo di aver fatto il lavoro. Il login era sicuro. Quella fiducia mi ha reso più lento nel sospettare SQLi quando è apparsa l'anomalia delle prestazioni. Se l'intera app fosse stata vulnerabile, potrei aver riconosciuto il pattern più velocemente.

Quarto: Il gap del proxy creato dalla terminazione TLS non è solo una questione di architettura della sicurezza — è una questione di architettura del rilevamento. Quando il tuo WAF e i tuoi log applicativi vedono cose diverse perché qualcosa in mezzo trasforma il traffico, hai bisogno di verifica esplicita che il tuo rilevamento copra il gap.

Tengo stampato quel frammento di log recuperato sopra il mio monitor. Non come punizione. Come promemoria che il gap tra "ho protetto le cose importanti" e "ho protetto tutto ciò che conta" è dove vivono gli attaccanti.

Letture consigliate