Menu

SQL Injection e dintorni: OWASP ZAP, Burp Suite e l'etica del pentest

9 min di lettura Manuel

L'SQL injection è la vulnerabilità più studiata della storia della sicurezza informatica, e questo è un po' la sua vergogna: sessant'anni dopo che è stata scoperta, chi la introduce ancora fa saltare siti ogni giorno.

Non è un errore esotico. È un errore che si commette per pigrizia, perché la strada veloce è concatenare stringhe e quella sicura richiede di fermarsi a pensare.

Cosa succede davvero

Il punto è che SQL è un linguaggio, e un linguaggio accetta frasi. Se l'input dell'utente viene incollato dentro una frase SQL, l'utente non sta scrivendo un valore: sta scrivendo codice.

Una domanda normale:

SELECT nome FROM utenti WHERE email = 'pippo@esempio.it'

Con l'input che passa grezzo, l'utente può chiudere la virgoletta e aggiungere condizioni:

-- input: pippo@esempio.it' OR '1'='1
SELECT nome FROM utenti WHERE email = 'pippo@esempio.it' OR '1'='1'

La query restituisce tutti gli utenti, perché la condizione è sempre vera. Non è un bug: è la query che funziona esattamente come scritta, con un input che nessuno aveva previsto.

Da lì in poi si passa a UNION, a estrazione di dati, in alcuni casi a esecuzione di comandi sul sistema operativo.

Il buco vero è quasi sempre lo stesso

Nella pratica, il 90% delle SQL injection che trovi hanno la stessa causa: codice che mette input dentro una stringa.

// ❌ concatenazione: l'input diventa parte della query
$sql = "SELECT * FROM utenti WHERE nome = '" . $_GET['nome'] . "'";
$risultato = $wpdb->get_results($sql);

// ❌ interpolazione: identico problema, solo più elegante
$sql = "SELECT * FROM utenti WHERE nome = '{$nome}'";

// ✅ prepared statement: il driver tratta l'input come DATO, mai come codice
$stmt = $pdo->prepare("SELECT * FROM utenti WHERE nome = ?");
$stmt->execute([$_GET['nome']]);

La differenza fra la terza riga e le prime due non è una questione di stile. Nel prepared statement l'input viene inviato separatamente dal codice SQL: il database lo tratta come una stringa e basta, non lo interpreta.

Per questo un prepared statement non è "una protezione": è l'assenza di una condizione. Non c'è codice da indovinare, non c'è protezione da indovinare.

Perché succede ancora

Il motivo reale è che la concatenazione è comoda e si sente intuitiva. $wpdb->query() accetta una stringa, e costruire un prepared statement richiede due righe. Su un progetto con una scadenza, la riga comoda vince.

C'è anche il motivo peggiore: l'idea che l'input sia "già pulito". Il campo @ in un modulo non è un utente, è qualcosa che passa dal browser. Se la validazione sta nel JavaScript del form, non è una validazione: è un suggerimento.

Cercare SQL injection in modo sistematico

A mano si cercano pattern, ma per un lavoro serio servono strumenti che provano centinaia di varianti al posto tuo.

Burp Suite: la Requests tab

Burp è lo standard per il lavoro manuale e semi-automatico. Il ciclo di lavoro:

  1. Navigi il sito con il browser configurato con il proxy di Burp
  2. Burp intercetta ogni richiesta e la mostra nella Proxy → HTTP history
  3. Cerchi le richieste che hanno parametri
  4. Ne selezioni una, la mandi a Repeater
  5. In Repeater modifichi il parametro e osservi la risposta

Il passaggio chiave è il confronto. Metti l'apostrofo nel parametro:

# baseline
nome=prova

# primo test: l'apostrofo
nome=prova'

Se la risposta cambia con un errore di sintassi SQL, hai trovato qualcosa. Con ' ricevi tipicamente:

You have an error in your SQL syntax; check the manual that
corresponds to your MySQL server version for the right syntax to use

Quel messaggio è un treasure map: conferma che l'input finisce in una query.

Poi prosegui con i classici test booleani, cercando una differenza di comportamento:

' OR '1'='1' -- commento
' AND '1'='2' -- commento

Se la prima variante restituisce più righe e la seconda meno, hai una query iniettabile. In MySQL il commento è -- (con lo spazio finale) oppure #; in SQL Server --; in PostgreSQL --.

C'è anche il tempo, che è il metodo più silenzioso:

' AND SLEEP(5) -- commento

Se la risposta arriva cinque secondi dopo, l'input viene eseguito dal database. Non cambia nulla a schermo, ma la risposta ritarda: è il test più affidabile quando la pagina restituisce sempre gli stessi dati.

OWASP ZAP: la scansione automatica

ZAP automatizza quello che faresti a mano, ed è gratuito. La modalità utile per iniziare è la passive scan, che osserva il traffico del browser mentre navighi e segnala quello che sembra pericoloso, senza inviare nulla di aggressivo.

# Avvia ZAP in ascolto come proxy
zaproxy -daemon -port 8080

# Oppure, per una singola pagina, la spider di ZAP
zap-baseline.py -t https://target.example -r passive-scan-report.html

La passiva scan è il punto di partenza corretto perché lavora su ciò che il sito fa davvero: non inventa URL, guarda quelli che esistono. Produce un report HTML con le occorrenze, e da lì scegli tu cosa approfondire.

Per andare oltre serve la active scan, che ZAP chiama ascan. Va usata con cautela, e solo su sistemi per cui hai l'autorizzazione scritta. Il motivo è tecnico, non etico: le tecniche attive possono alterare dati, e su un database reale una DROP TABLE non è uno scherzo.

# Active scan mirata, solo a una parte del sito
zap-cli.py -t https://target.example -a ascan \
  -T 60 \
  --config=attack-params=id,name \
  -r active-report.html

--config=attack-params limita l'attacco ai parametri che hai scelto: utile per non martellare l'intera superficie.

ZAP ha anche le Active Rules, che puoi scrivere tu. Se hai SQL injection nel tuo software e vuoi che chi lo sviluppa dopo lo trovi prima di te, puoi scrivere una regola che invia ' OR 1=1-- a ogni parametro numerico e verifica se la risposta cambia. È un modo pragmatico di rendere obbligatoria la verifica.

Difesa: la checklist reale

In ordine di efficacia, dall'alto verso il basso:

Prepared statement, sempre. Non "quando l'input è pericoloso": sempre, anche per una stringa che non può contenere injection. Il costo è nullo e ti toglie la necessità di pensare.

Validazione per tipo. Un ID deve essere un intero, e absint() in WordPress o intval() in PHP lo garantiscono. Un campo email deve corrispondere a un pattern, non "non contenere apostrofi".

Principio del privilegio minimo. L'utente del database non deve poter fare DROP, e in molte applicazioni non ha nemmeno bisogno di SELECT *. UnaInjection che legge tutto è già brutta; una che non può scrivere è molto meno pericolosa.

Nascondere gli errori. Mostrare all'utente "errore di sintassi SQL nella riga 3" è regalare all'attaccante una mappa. Gli errori vanno nel log, non a schermo.

WAF come rete di sicurezza, non come soluzione. ModSecurity o il WAF di Cloudflare bloccano i pattern più grossolani. Non fermano un attaccante che sa cosa sta facendo, ma abbassano il rumore da mille a dieci richieste al giorno.

Etica e identità hacker: le cose che nessun tutorial scrive

Fin qui è tecnica. Il resto è la parte che decide se una persona diventa un professionista o un problema per gli altri.

La differenza non è tecnica

Non esiste una linea di demarcazione tecnica tra "hacker" e "criminale". Le stesse identiche strumenti — Burp, ZAP, sqlmap — servono a chi cerca falle da segnalare e a chi le cerca per estorcere. Gli strumenti sono gli stessi. L'intento, l'autorizzazione e la trasparenza no.

Quindi la domanda giusta non è "è legale?" ma tre, in quest'ordine:

  1. Ho l'autorizzazione scritta? Non verbale, non "mi hanno detto che va bene", non "è il sito di un mio amico". Scritta, con un ambito definito: quali URL, quali orari, che cosa posso fare se trovo qualcosa.
  2. Sono dentro il perimetro? Un test che esce dai pattuìti è una violazione anche se il pentest è autorizzato, e in molti contratti è il modo più semplice per perdere la copertura assicurativa.
  3. Cosa faccio quando trovo una cosa?

Il punto 3 è quello che definisce l'identità

La differenza tra un ricercatore di sicurezza e un criminale non è nella tecnica: è in cosa succede dopo la scoperta.

Chi lavora serio trova una SQL injection e scrive un'email chiara: dove, come si riproduce, qual è l'impatto, una proposta di correzione. Pubblico o privato, ma in ogni caso prima di usare il dato. Chi non fa questo non è discreto: sta solo decidendo quando conviene.

Le regole non scritte del settore, quelle che valgono:

  • Non prendi i dati. Un database con le credenziali degli utenti è una responsabilità, non un bottino. Se li hai letti per dimostrare l'impatto, non li tiri fuori per farne spettacolo.
  • Non usi l'accesso per persistenza. Un account creato "per sicurezza" è un accesso non autorizzato, punto.
  • Non pubblichi il dettaglio prima che sia corretto. Un exploit funzionante su un sito ancora vulnerabile è un danno, non una ricerca.
  • Non smetti al primo CVE. Chiunque può trovare un errore con sqlmap. Il valore sta nel capire perché c'è, e nel rendersi inutile come bersaglio.

Hacker, white hat, black hat: le etichette servono a poco

Le etichette sono comode ma imprecise. La distinzione che regge è più semplice e più utile: l'hacker è chi capisce come funzionano le cose, e non è ancora deciso cosa farne. Diventa white hat quando mette le proprie capacità al servizio di chi ne ha bisogno e ne risponde. Diventa black hat quando inizia a usarle per qualcun altro.

E c'è una quarta via, che è quella che il settore onora di più: il bug bounty. Piattaforme come HackerOne e Bugcrowd pagano per vulnerabilità valide, con regole precise, dentro un perimetro dichiarato. Non è carità: è un mercato, e un mercato è il modo più efficace che abbiamo trovato per mettere in competenza chi cerca buchi e chi ha motivazione a trovarli.

La regola che non si nega

Niente di tutto questo vale senza il permesso. Non è una morale, è una constatazione operativa: la differenza tra un tester e un criminale è un documento, e quel documento è l'unica cosa che ti protegge quando le cose vanno male. L'attaccante non ha bisogno di documenti, e questo è il suo vantaggio competitivo. Tu puoi avere la stessa abilità e non avere nulla da perdere: ed è l'unico terreno in cui vale la pena giocare.

Se vuoi imparare, fallo su sistemi tuoi. Una macchina virtuale, un WordPress di prova, un laboratorio. Costa un pomeriggio e ti lascia la libertà di sbagliare, che è l'unico modo in cui si impara davvero.