Guida SQL Injection

1. Introduzione

1.1 Le applicazioni web e i DBMS SQL

Sono sempre più diffuse le applicazioni web che si appoggiano su DBMS (Database Management System), in molti casi di tipo SQL (Structured Query Language), per esempio MySQL. SQL è dunque uno dei principali linguaggi utilizzati per interagire con i database.

Gli utenti interagiscono con l’applicazione web richiedendo svariati servizi, come l’autenticazione, o semplicemente delle informazioni. Per esempio un utente che visita il sito web di una libreria può voler visualizzare l’elenco dei libri disponibili.

In questi casi l’applicazione web fa da tramite tra l’utente, che sta richiedendo un informazione (oppure un servizio che comunque richiede il recupero di una determinata informazione), e il database in cui risiede informazione.

Nell’esempio dell’utente che richiede l’elenco dei libri al sito della libreria, quest’ultimo esegue a sua volta una query SQL per richiedere al database le informazioni necessarie per soddisfare la richiesta dell’utente, in questo caso i libri.

 

1.2 L’attacco SQL Injection

Uno dei principali attacchi che colpisce le applicazioni web appena descritte

è SQL Injection il quale, sfruttando l’inefficienza dei controlli sui dati ricevuti da parte dell’utente, inserisce codice maligno all’interno di una query SQL alterandone quindi il suo significato.

Alterando una query SQL, l’utente malintenzionato potrebbe essere in grado di autenticarsi con privilegi che non gli competono (per esempio autenticarsi come amministratore) anche senza essere in possesso delle credenziali d’accesso, oppure potrebbe essere in grado di visualizzare e/o alterare dati sensibili contenuti all’interno del database.

 

1.3 La demo

Verrà fatto riferimento alla demo situata al seguente indirizzo:

http://sqlidemo.altervista.org

Il quale mostra alcuni esempi di pagine web scritte in PHP vulnerabili a determinati attacchi SQL Injection, mostrando le query SQL che vengono eseguite per soddisfare le richieste che partono dall’utente. Il sito mostra inoltre come rendere tali pagine immuni agli attacchi a cui sono soggette.

L’intero codice con cui è realizzato è disponibile presso il seguente indirizzo:

https://github.com/ShinDarth/sql-injection-demo

 

2. Tipologie di attacco e metodi di protezione

2.1 Attacco ad un classico login e la funzione escape

Come primo esempio, prendiamo in esame la seguente pagina web:

http://sqlidemo.altervista.org/login1.php

Il compito di questa pagina è quello di autenticare gli utenti registrati, le cui credenziali sono memorizzati nella tabella users del database.

All’interno della pagina vi è dunque un form che prende in input due parametri, un username ed una password. Una volta ricevuti in input tali dati, lo script PHP dovrà verificare se esiste un utente nella tabella users del database avente username e password identici a quelli inviati dal client e in tal caso procedere con l’autenticazione.

Prendiamo in esame il codice PHP dello script, inizialmente nelle variabili $username e $password vengono memorizzati rispettivamente l’username e la password ricevuti in input:

$username = $_POST['username'];
$password = $_POST['password'];

a questo punto viene preparata la query:

$query = sprintf("SELECT * FROM users WHERE username = '%s' AND password = '%s';",
                 $username,
                 $password);

la funzione sprintf() non fa altro che sostituire ai due %s le variabili $username e $password, ottenendo pertanto la seguente query:

SELECT * FROM users WHERE username = '$username' AND password = '$password';

dopo di che la query viene eseguita:

$result = mysqli_query($connection, $query);

mysqli_query() è una funzione che prende in input una connessione ad un database di tipo mysqli, nel nostro caso $connection, ed una query,nel nostro caso memorizzata nella variabile $query, e restituisce il risultato della query che noi andremo a memorizzare nella variabile $result (per approfondire: http://www.php.net/mysqli_query).

Se la query non ha prodotto risultato, la variabile $result conterrà il valore booleano FALSE. Altrimenti essa conterrà un oggetto di tipo mysqli-result (per approfondire: http://www.php.net/manual/en/class.mysqli-result.php).

Adesso ci basterà controllare l’attributo intero $num_rows di $result che indica il numero di righe che sono state ritornate dalla query, se è maggiore di 0 vuol dire che nella tabella users c’è un utente con credenziali $username e $password:

if ($result->num_rows > 0)
{
    echo "Authenticated as " . $username;

    // PROCEDO CON L'AUTENTICAZIONE
}
else
{
    echo "Wrong username/password combination.";

    // NON EFFETTUO L'AUTENTICAZIONE
}

Questo script PHP è dunque in grado di effettuare l’autenticazione, ma è soggetto ad un attacco SQL Injection. Analizziamo il caso in cui verrebbero passati un $username qualsiasi, per esempio “admin”, e la seguente stringa come $password:

1′ OR ‘1’=’1

la query sarebbe sempre:

SELECT * FROM users WHERE username = '$username' AND password = '$password';  

sostituendo in questo caso diventerebbe:

SELECT * FROM users WHERE username = 'admin' AND password = '1' OR '1'='1';

intuitivamente, essendo la sotto-condizione ‘1’ = ‘1’ sempre vera, l’intera condizione dopo la clausola WHERE risulta sempre vera e come risultato della query verranno restituite tutte le righe della tabella users. Di conseguenza, anche la clausola $result->num_rows > 0 sarebbe sempre vera e il risultato sarebbe l’autenticazione dell’utente $username, qualunque esso sia.

Per prevenire un attacco di questo genere possiamo ricorrere alla funzione mysqli_real_escape_string() così definita: string mysqli_real_escape_string ( mysqli $link , string $escapestr ) tale funzione prende dunque in input una connessione ad un database MySQL $link e una stringa $escapestr e “prepara” quest’ultima ad essere inserita all’interno di una query SQL effettuando l’escape di tutti i caratteri potenzialmente dannosi (per esempio l’apice singolo ). La connessione con il database serve alla funzione per identificare il character set in uso.

Nella pagina http://sqlidemo.altervista.org/login2.php viene sostituita la riga di codice:

$query = sprintf("SELECT * FROM users WHERE username = '%s' AND password = '%s';",
                 $username,
                 $password);

con la seguente, che fa uso della funzione mysqli_real_escape_string():

$query = sprintf("SELECT * FROM users WHERE username = '%s' AND password = '%s';",
                 mysqli_real_escape_string($connection, $username),
                 mysqli_real_escape_string($connection, $password));

rendendo lo script immune all’attacco SQL Injection descritto sopra. Infatti questa volta se passassimo come $usernameadmin, e la stringa 1′ OR ‘1’=’1 come $password, la query:

SELECT * FROM users WHERE username = '$username' AND password = '$password';

questa volta diventerebbe:

 SELECT * FROM users WHERE username = 'admin' AND password = '1\' OR \'1\'=\'1';

non producendo nessun risultato.

 

2.2 Attacco per recuperare dati sensibili

Nella pagina http://sqlidemo.altervista.org/books1.php è presente un form che consente di visualizzare dei libri cercando per titolo e/o per autore. Lo script PHP che c’è alla base è molto semplice, i parametri vengono passati in modalità GET quindi ciò che l’utente immette come titolo e come autore verrà ricevuto dallo script rispettivamente nelle variabili $_GET[‘title’] e $_GET[‘author’]. Lo script riceve in input questi parametri, prepara la query:

$query = sprintf("SELECT * FROM books WHERE title = '%s' OR author = '%s';",
                 $_GET['title'],
		 $_GET['author']);

dopo di che effettua la query ed esegue un ciclo per tutte le righe trovate:

$result = mysqli_query($connection, $query);

while
(($row = mysqli_fetch_row($result)) != null
{
    printf("<tr><td>%s</td><td>%s</td><td>%s</td></tr>",
    $row[0], $row[1], $row[2]);
}

la funzione mysqli_fetch_row() prende in input il risultato della query ed estrae una riga, sotto forma di array (per approfondire: http://www.php.net/manual/en/mysqli-result.fetch-row.php).

Ad ogni iterazione viene quindi estratta una riga e memorizzata all’interno dell’array $row. Il ciclo si interrompe quando non ci sono più righe da mostrare. Supponiamo che come author venga inserito “Margherita Hack”, la query sarà:

SELECT * FROM books WHERE title = '' OR author = 'Margherita Hack';

E verranno stampate tutte le righe della tabella books che soddisfano questa condizione.

Anche questo script è soggetto ad un attacco SQL Injection. Supponiamo adesso che come author venga passata la seguente stringa:

‘ UNION SELECT * FROM users WHERE ‘1’=’1

la query che si ottiene sarà la seguente:

SELECT * FROM books WHERE title = '' 
OR author = ''UNION SELECT * FROM users WHERE '1'='1';

pertanto verrà visualizzato il contenuto della tabella users!

Anche in questo caso, per evitare di essere vulnerabili a questo tipo di attacco possiamo ricorrere alla funzione mysqli_real_escape_string() e sostituire la riga:

$query = sprintf("SELECT * FROM books WHERE title = '%s' OR author = '%s';",
                 $_GET['title'],
                 $_GET['author']);

con la seguente:

$query = sprintf("SELECT * FROM books WHERE title = '%s' OR author = '%s';",
                  mysqli_real_escape_string($_GET['title']),
                  mysqli_real_escape_string($_GET['author']));

in questo caso passando ‘ UNION SELECT * FROM users WHERE ‘1’=’1 come autore causerebbe l’esecuzione della query:

SELECT * FROM books WHERE title = '' 
OR author = '\' UNION SELECT * FROM users WHERE \'1\'=\'1';

chiaramente innocua.

 All’indirizzo http://sqlidemo.altervista.org/books2.php è disponibile la versione “sicura” dello script PHP preso in esame.

 

2.3 Attacco a login di tipo numerico e controllo sui tipi di dato

A questo punto ci si potrebbe chiedere se è sufficiente utilizzare la funzione di escape per proteggersi dagli attacchi di tipo SQL Injection. La risposta è negativa.

Consideriamo lo scenario a pagina:

http://sqlidemo.altervista.org/login3.php

in cui vi è un login di tipo numerico, ovvero tutti gli account e le password degli utenti sono valori numerici. L’utente che vuole autenticarsi è pertanto tenuto a immettere il proprio identificativo numerico e il proprio PIN, che vengono memorizzati rispettivamente come $client e $pin:

$client = $_POST['client'];
$pin = $_POST['pin'];

la query viene preparata nel seguente modo:

$query = sprintf("SELECT * FROM clients WHERE id = %s AND pin = %s;",
                  mysqli_real_escape_string($connection, $client),
                  mysqli_real_escape_string($connection, $pin));

dunque viene applicata la funzione di escape alle stringhe $client e $pin, da notare inoltre che nella query:

SELECT * FROM clients WHERE id = %s AND pin = %s;

i valori che saranno associati al campo id e pin della tabella clients non vengono racchiusi tra i singoli apici (come invece avveniva nelle query degli esempi mostrati precedentemente), perché la tabella clients è costituita da valori interi e non da stringhe.

Dopo la preparazione della query, quest’ultima viene eseguita e viene controllato se esiste un client con le credenziali identiche a quelle ricevute in input. Se l’esito è positivo, si procede con l’autenticazione, altrimenti si restituisce errore:

$result = mysqli_query($connection, $query);

if ($result->num_rows > 0)
{
    echo "Authenticated as " . $client;
    // PROCEDO CON L'AUTENTICAZIONE
}
else
{
    echo "Wrong client/PIN combination.";
    // NON EFFETTUO L'AUTENTICAZIONE
}

Questo è uno dei casi in cui, nonostante sia applicata la funzione di escape su entrambi i parametri immessi dall’utente, lo script è comunque vulnerabile ad attacco SQL Injection.

Infatti basta passare come $client l’id del client per cui ci si vuole autenticare indebitamente e come $pin la seguente stringa:

1 OR 1=1

per essere autenticati.

Nello specifico, passando ad esempio 1234 come $client e 1 OR 1=1 come $pin, la query:

SELECT * FROM clients WHERE id = %s AND pin = %s;

sostituendo diventerebbe:

SELECT * FROM clients WHERE id = 1234 AND pin = 1 OR 1 = 1;

ed essendo la condizione 1 = 1 sempre vera, anche in questo caso verrebbero restituite tutte le righe della tabella clients e la condizione $result->num_rows > 0 sarebbe ovviamente vera.

Il problema si risolve facilmente facendo un controllo rigoroso sui tipi di dato. Nello specifico, in questo caso ci si aspettano valori prettamente numerici. Siamo quindi interessati ad accettare $client e $pin immessi dall’utente solo nel caso in cui essi rappresentano numeri.

La funzione is_numeric() di PHP svolge proprio il compito che ci serve: essa infatti prende in input una variabile $var qualsiasi e restituisce un valore booleano pari a TRUE se $var è un numero, FALSE altrimenti.

La pagina:

http://sqlidemo.altervista.org/login4.php

presenta una versione modificata dell’esempio che abbiamo appena visto, in cui introduce l’utilizzo della funzione is_numeric() per rendere lo script immune all’attacco SQL Injection mostrato sopra.

Banalmente, quello che avviene è che si procede solo se $client e $pin sono valori numerici:

$client = $_POST['client'];
$pin = $_POST['pin'];

if (is_numeric($client) && is_numeric($pin))
{
    // PROCEDO
    // ...
}
else
{
    echo "Client ID and PIN must be numeric values.";
    // NON PROCEDO
}

 

La funzione is_numeric() appartiene alla famiglia delle Variable-handling Functions (funzioni per la gestione della variabili).

Le altre funzioni appartenenti a tale famiglia svolgono compiti analoghi a is_numeric(), per esempio:

is_int() – controlla se la variabile che riceve in input è un numero intero

is_float() – controlla se la variabile che riceve in input è un numero reale

is_string() – controlla se la variabile che riceve in input è una stringa

Per un elenco completo di tutte le Variable-handling Functions: http://www.php.net/manual/en/ref.var.php

È possibile effettuare un controllo più preciso sui dati che si ricevono in input, servendosi delle espressioni regolari, che verranno mostrate in seguito.

 

2.4 Attacco con concatenazione query aggiuntive

Un altro attacco, meno diffuso, consiste nel concatenare ulteriori query a quella esistente (multiple-statements). Per esempio nella query della libreria (http://sqlidemo.altervista.org/books1.php):

SELECT * FROM books WHERE title = '%s' OR author = '%s';

se passassimo come author il seguente valore:

'; DELETE FROM books WHERE '1'='1

otterremmo, in teoria, l’esecuzione della seguente query:

SELECT * FROM books WHERE title = '' OR author = ''; DELETE FROM books WHERE '1'='1';

Questo attacco non è molto diffuso in quanto, nell’attuale versione di PHP, la funzione mysqli_query() non consente l’esecuzione di query multiple. Di conseguenza, se il programmatore vuole effettuare query multiple deve esplicitamente fare ausilio dell’apposita funzione mysqli_multi_query()

Ciò rende la maggior parte dei siti web, che sonoscritti in PHP e che si appoggiano ad un database di tipo MySQL, automaticamente immuni a questo tipo di attacco perché solitamente il programmatore tende ad usare mysqli_multi_query()solamentenel caso in cui necessita di effettuare query multiple, a vantaggio della tradizionale funzione mysqli_query().

 

2.5 Cenni sulle espressioni regolari

Un ulteriore misura di protezione contro attacchi di tipo SQL Injection consiste nell’utilizzo delle espressioni regolari. Con il termine espressione regolare (o regexp) ci riferiamo ad una sequenza di simboli, quindi di fatto una stringa, che a sua volta identifica un insieme di stringhe (per approfondire: http://it.wikipedia.org/wiki/Espressione_regolare).

Molto spesso capita che i dati che l’applicazione web si aspetta di ricevere da parte dell’utente possono essere descritti da un’espressione regolare. Per esempio, potremmo essere interessati ad accettare una stringa inserita dall’utente solo se essa è costituita esattamente da un determinato numero di caratteri letterali e/o numerici.

In PHP possiamo ricorrere all’utilizzo della funzione preg_match() così definita:

int preg_match (string $pattern, string $subject, [altri parametri opzionali])

la quale prende in input un espressione regolare (racchiusa tra i simboli / e /) $pattern e una stringa $subject e restituisce 1 se la stringa $subject appartiene all’insieme delle stringhe descritte dall’espressione regolare $pattern, 0 altrimenti (per approfondire: http://www.php.net/manual/en/function.preg-match.php).

Per modellare un’espressione regolare bisogna far uso dei “metacaratteri”, di seguito ne vengono riportati alcuni:

– [] le parentesi quadre, destinate a contenere una ‘classe’ di caratteri, per esempio:

     [a-z] in questo modello è presente un intervallo di caratteri ( il segno -, sta per “dalla a alla z”)

      [0-9] in questo modello è presente invece un intervallo di numeri

{} le parentesi graffe, che indicano il numero esatto o l’intervallo di occorrenze di un carattere o di un gruppo di caratteri, per esempio:

     [a-z]{1,3} i caratteri contenuti nella classe devono essere presenti da1a3 volte;

     [a-z]{4} i caratteri contenuti nella classe devono essere esattamente 4 volte;

     [a-z]{2,} i caratteri contenuti nella classe devono essere presenti minimo 2 volte;

^ che indica l’inizio della stringa (o, se all’interno di una classe di caratteri, la negazione della stessa)

$ che indica la fine della stringa

Questi sono solo alcuni tra i tanti metacaratteri con cui comporre un’espressione regolare (per approfondire: http://www.php.net/manual/en/regexp.introduction.php), ma sono sufficienti per mettere in pratica il seguente esempio:

supponiamo di dover accettare solo stringhe formate da un numero che va da 2 a 4 caratteri letterali minuscoliiniziali, concatenati a 2 caratteri numerici finali; siamo quindi interessati ad accettare solo stringhe come “abcd45”, “mn25”, “efg04”.

Innanzitutto la nostra espressione regolare dovrà iniziare con il simbolo ^ e terminare con il simbolo $, in modo da prendere in considerazione tutte e sole le stringhe appartenenti all‘insieme di stringhe sopra definito. Ci interessa inoltre che le stringhe inizino con una sequenza di caratteri letterali minuscoli compresa tra 2 e 4, quindi [a-z]{2,4} e che termini con esattamente due caratteri numerici, quindi [0-9]{2}. Mettendo insieme quanto appena detto otteniamo:

^[a-z]{2,4}+[0-9]{2}$

siccome le espressioni regolari da dare in input alla funzione preg_match() devono essere racchiuse tra i simboli / e /, otteniamo:

/^[a-z]{2,4}+[0-9]{2}$/

a questo punto, supponendo di voler validare una stringa memorizzata all’interno della variabile $str, ci basterebbe scrivere:

$pattern = "/^[a-z]{2,4}+[0-9]{2}$/";

if (preg_match($pattern, $str))
{
    // stringa $str valida, proseguo...
}
else
{
    // errore! mi fermo
}

All’indirizzo http://sqlidemo.altervista.org/regexp.php è disponibile uno script PHP che permette di immettere un’espressione regolare ed una stringa, verificando se tale stringa appartiene all’insieme delle stringhe definite dall’espressione regolare.

 

3. SQL-Map

Sqlmap è un software open source scritto in python. Si tratta di uno strumento per penetration testing usato per trovare vulnerabilità agli attacchi di tipo SQL Injection. http://sqlmap.org/

Con sqlmap è possibile effettuare dei test su un parametro di una pagina PHP e verificare se esso è vulnerabile ad attacchi di tipo SQL Injection.

Nel caso di dati passati in modalità POST bisogna copiare la POST request in un file di testo, e lanciare sqlmap dandogli in pasto il file di testo contenente la POST request.

Invece nel caso di dati passati in modalità GET è possibile lanciare direttamente sqlmap dandogli in pasto l’url della pagina web e del parametro su cui si vogliono cercare vulnerabilità.

Ad esempio, la pagina vista in precedenza:

http://sqlidemo.altervista.org/books1.php

riceve in input due parametri in modalità GET, ovvero author e title. Se volessimo cercare vulnerabilità sul parametro author, dovremmo avviare sqlmap con il seguente comando:

sqlmap -u "http://sqlidemo.altervista.org/books1.php?author="

sqlmap ci restuisce:

[17:21:34] [INFO] GET parameter 'author' is 'MySQL UNION query (NULL) - 1 to 10 columns' injectable 

proprio perché tale pagina, come visto in precedenza, è vulnerabile a un attacco SQL Injection.

Se invece lanciamo sqlmap dandogli in pasto la pagina books2.php (la versione “sicura” della pagina books1.php) sul medesimo parametro, ovvero dando il comando:

sqlmap -u “http://sqlidemo.altervista.org/books2.php?author=

sqlmap ci restituisce:

[17:25:36] [WARNING] GET parameter ‘author’ is not injectable

perché non trova nessuna vulnerabilità.

È possibile ottimizzare sqlmap in modo da aumentare le prestazioni ed ottenere i risultati in minor tempo. Ad esempio, aggiungendo i parametri:

-o -threads 8

è possibile scegliere il numero di thread, in questo caso 8.

Inoltre, dal momento in cui sqlmap è utilizzato soprattutto per verificare la sicurezza dei propri sistemi, solitamente si conosce la tipologia del DBMS su cui si vanno ad effettuare i test (nel nostro caso MySQL) e specificando tale informazione a sqlmap si aumentano ulteriormente le prestazioni. Nel nostro caso dunque aggiungiamo il parametro:

--dbms MySQL

quindi il comando per testare il parametro author della pagina books1.php diventerebbe:

sqlmap -u “http://sqlidemo.altervista.org/books1.php?author=” -o -threads 8 –dbms MySQL

riducendo notevolmente i tempi di esecuzione.

In presenza di un parametro vulnerabile, sqlmap permette inoltre di visualizzare tutti i database sel sistema in esame, incluse le tabelle e le loro colonne.

Per visualizzare i database, basta aggiungere il parametro:

--dbs

per esempio:

sqlmap -u "http://sqlidemo.altervista.org/books1.php?author=" -o -threads 8 --dbms MySQL --dbs

Per visualizzare tutte le tabelle di un determinato database, bisogna aggiungere:

-D nomeDelDatabase --tables

per esempio:

sqlmap -u "http://sqlidemo.altervista.org/books1.php?author=" -o -threads 8 --dbms MySQL --dbs -D my_sqlidemo --tables

Per visualizzare le colonne di una determinata tabella, bisogna aggiungere:

-D nomeDatabase -T nomeTabella --columns

per esempio:

sqlmap -u "http://sqlidemo.altervista.org/books1.php?author=" -o -threads 8 --dbms MySQL --dbs -D my_sqlidemo -T users --columns

Infine, per visualizzare il contenuto di una determinata tabella, basta aggiungere:

-D nomeDatabase -T nomeTabella --dump --start 1 --stop 5

con –start e –stop si indica il range delle righe da visualizzare (in questo caso da 1 a 5). Omettendo tali valori verranno mostrate tutte le righe. Per esempio, se vogliamo visualizzare le prime 8 righe della tabella users del database my_sqlidemo, digitiamo:

sqlmap -u "http://sqlidemo.altervista.org/books1.php?author=" -o -threads 8 --dbms MySQL --dbs -D my_sqlidemo -T users --dump --start 1 --stop 8

Queste sono solo alcune delle potenzialità di sqlmap, per approfondire l’argomento visitare la pagina della documentazione ufficiale:

https://github.com/sqlmapproject/sqlmap/wiki

About Shin

Programmatore per passione. Mi piace condividere qualsiasi idea o informazione utile, per questo motivo ho realizzato il blog.