Introduzione

Una vulnerabilità di tipo SQL Injection si verifica quando un utente, interagendo con funzionalità di un’applicazione che operano su un database, è in grado di iniettare codice SQL nella query che viene eseguita. Questo tipo di vulnerabilità è spesso riscontrabile nelle applicazioni Web, ma può verificarsi anche in applicazioni non basate sul protocollo HTTP, ma che comunque interagiscono con un database SQL. In generale, questa vulnerabilità si verifica quando l’input utente non viene correttamente gestito prima di essere utilizzato all’interno del flusso applicativo. Ad esempio, un’applicazione è vulnerabile se costruisce una query SQL utilizzando un input controllabile dall’utente senza sanificarlo.

Per capire meglio come questa vulnerabilità possa verificarsi, creiamo una semplice applicazione Web in PHP che interagisce con un database MySQL. L’applicazione ha lo scopo di interrogare un DB per la gestione di alcuni prodotti e degli utenti. Il database, in queto caso, è piuttosto piccolo e consiste in due tabelle denominate products e users. La struttura delle tabelle è descritta di seguito.

products

id product_name
1 example

users

id name password
1 Paolo x

Il codice sorgente della web application è mostrato nella figura sottostante.

alt src

La prima parte del codice (righe 36-40) serve per instaurare la connessione tra l’applicazione Web e il database MySQL. Mentre la parte evidenziata (righe 42-47) rappresenta la gestione dell’input utente. Come si può notare, l’applicazione controlla se il parametro GET p_name è impostato, in caso affermativo costruisce una query concatenando una stringa predefinita con il parametro p_name. Il problema è che tale parametro è controllabile da un utente e viene concatenato con la variabile $sql senza subire alcuna modifica; infatti un utente può infrangere la sintassi prevista della query utilizzando il carattere '.

alt error

Analisi della vulnerabilità e Exploitation

La query che l’applicazione intende eseguire è la seguente:

SELECT * FROM products WHERE product_name LIKE '%user-input-goes-here%'

Come si può notare nella figura sottostante, se si passa un valore come sticker, l’applicazione restituisce tutti i prodotti che contengono questa stringa nel campo product_name.

alt app

Infatti, se l’input utente fosse sticker, la query diverrebbe SELECT * FROM products WHERE product_name LIKE '%sticker%' e poiché ci sono due prodotti che contengono la parola sticker, l’applicazione li restituisce all’interno di una tabella html. Se invece proviamo a fare una ricerca con un valore come not-existent-products l’applicazione non troverà alcun risultato.

alt app

Abbiamo visto che iniettando il carattere ' possiamo interferire con la query originale, ma dobbiamo tenere presente che la sintassi della query SQL finale deve essere corretta, altrimenti riceveremo solo un errore dall’applicazione. Abbiamo già detto che un utente può interferire con la query usando il carattere ', vediamo ora cosa succede usando questo payload not-existent-products' or '1'='1' -- - come valore di ricerca per il parametro GET.

alt app

Come si può vedere, l’applicazione restituisce alcuni risultati che non dovrebbero esserci. Questo accade perché siamo riusciti a iniettare il carattere ', modificando così la sintassi e anche la logica della query originale e di conseguenza anche il comportamento della funzionalità di ricerca. In effetti, siamo riusciti a modificare la query inserendo una condizione sempre vera or '1'='1'.

SELECT * FROM products WHERE product_name LIKE '%not-existent-products' or '1'='1' -- -%'

Da notare anche l’utilizzo dei caratteri -- nel payload, questo è uno dei modi per usare i commenti in MySQL (vale anche per altri DBMS). L’utilità del commento è quella di evitare che tutto ciò che verrà concatenato dopo il punto di injection possa ancora interferire con la query (ad esempio in questo caso evita che i caratteri %' possano andare a creare problemi di sintassi).

Vediamo ora come un aggressore può sfruttare la vulnerabilità per estrarre dati arbitrari dal database. Una strategia per estrarre i dati del DB è quella di fare in modo che l’applicazione li restituisca direttamente nel corpo della risposta HTTP, modificando la query originale per incorporare i dati che vogliamo estrarre. Per farlo, utilizzeremo l’operatore UNION, che può essere usato per combinare insieme i risultati di due o più istruzioni SELECT. Un vincolo importante è che ogni istruzione SELECT all’interno di UNION deve avere lo stesso numero di colonne. Come abbiamo detto precedentemente, nel database è presente un’altra tabella denominata users e per un ipotetico attaccante potrebbe essere interessante estrarre i dati da questa tabella. Idealmente quello che vogliamo fare è eseguire una query che restituisca oltre ai dettagli sui prodotti anche i dati dalla tabella user, questo è possibile in SQL con una query della forma SELECT a FROM B UNION SELECT c FROM D. Un payload valido è il seguente sticker' UNION SELECT name, password FROM users WHERE id=1 -- -, infatti la query finale iniettando questo payload sarebbe:

SELECT * FROM products WHERE product_name LIKE '%sticker' UNION SELECT name, password FROM users WHERE id=1 -- -%'

La prima istruzione SELECT recupera tutte le colonne (grazie all’utilizzo del carattere *) della tabella prodotti in base alla condizione LIKE, la seconda SELECT seleziona i campi nome e password dalla tabella users in base alla condizione id=1. L’operatore UNION unisce i risultati della prima SELECT con i risultati della seconda SELECT, che viene fatta sulla tabella users. Come detto in precedenza il numero di colonne della prima SELECT deve coincidere con il numero di colonne della seconda, in questo caso sono due colonne. L’ultima parte del payload --, come è stato già accennato precedentemente, rappresenta un commento SQL ed è utile per rimuovere la parte della query non controllabile. Attenzione a includere sempre uno spazio prima e dopo i trattini i quali sono richiesti da MySQL. Proviamo ora questo payload e osserviamo il comportamento dell’applicazione.

alt app

Come potete vedere l’applicazione ha ritornato i dati contenuti nella tabella users, in particolare lo username e la password dell’utente con id=1, dimostrando che è possibile estrarre dati arbitrari dal database dell’applicazione. Questo tipo di SQL injection è chiamato anche Union-Based SQL injection proprio perchè la logica si basa sull’operatore UNION.

Soluzione

Un metodo semplice e sicuro per costruire query SQL è quello di utilizzare le “query parametriche” o “prepared statements” evitando la concatenazione di stringhe con variabili controllabili dall’utente. In seguito si tratterà più nel dettaglio questo tipo di approccio.

Un’alternativa semplice e valida in questo specifico contesto è utilizzare una funzione specifica per effettuare l’escape dell’input utente prima di concatenarlo alla query. Ogni linguaggio normalmente fornisce una funzione dedicata per poter costruire le query in questo modo, nel nostro caso siccome l’applicazione è stata scritta in PHP bisogna utilizzare la funzione mysqli::real_escape_string. In questo modo i caratteri speciali come l’apice vengono correttamente gestiti e non interferiranno più con la sintassi della query SQL. Di seguito riportiamo la porzione di codice con il fix.

alt app

A questo punto se proviamo a sfruttare la vulnerabilità sull’applicazione aggiornata con il fix, non è più possibile interferire con la logica della query originale ed estrarre dati arbitrari dal database.

alt app

Conclusioni

In questo articolo abbiamo voluto presentare un semplice caso di studio per far comprendere quando si verifica una SQL Injection e come può essere sfruttata. Come spesso accade le vulnerabilità di SQL Injection sono dovute ad un input utente che non viene adeguatamente controllato prima di essere utilizzato in una query SQL, di conseguenza l’importanza di controllare tutti gli user input è cruciale per garantire la sicurezza di un’applicazione.