4. Database: SQL injections
Con tale termine intendiamo tutti gli attacchi ad un’applicazione Web – nel nostro caso PHP – che consentano di modificare l’interazione della stessa con il database: nel caso vengano eseguite, lato server, interrogazioni MySQL costruite su quanto ricevuto dal client, senza un controllo sull’input è possibile che un hacker crei, “tecnicamente” parlando, seri disastri.
4.1 Autenticazione
Primo esempio, autenticazione utente
Riporterò ora un primo, classicissimo, esempio.
Posto che la direttiva magic_quotes_gpc del php.ini sia settata ad OFF e non venga fatto l’escape del carattere “ ‘ “, studiamo il caso in cui l’autenticazione degli utenti di un’ipotetica applicazione Web PHP utilizzi la logica seguente. I dati passati allo script di autenticazione giungono direttamente da un form HTML opportuno.
Codice PHP:
$utente_autorizzato=0;
$sql=”SELECT * FROM tabella_utenti WHERE usr=’".$_POST[‘username’]."' AND pwd='".$_POST[‘password’]."'";
$query=mysql_query($sql,$db);
if ( mysql_num_rows($query) > 0 )
{
$utente_autorizzato=1;
}
Se username e password inserite lato client sono rispettivamente marco e miapass, la query assume la forma:
Codice PHP:
SELECT * FROM tabella_utenti WHERE usr=’marco’ AND pwd=’miapass’;
A questo punto, se nel database è presente un’entry che soddisfi a tale query, l’utente è autorizzato ad accedere all’applicazione.
La vulnerabilità è però in agguato: se username e password inserite lato client fossero, invece, qualsiasi_nome e qualsiasi_stringa' OR 1, la query assumerebbe la diabolica forma:
Codice PHP:
SELECT * FROM tabella_utenti WHERE usr=’qualsiasi_nome’ AND pwd=’qualsiasi_stringa’ OR 1;
Essendo la query sempre soddisfatta (ha valore di verità sempre 1), il suo risultato sarà l’intera lista degli utenti (tutte le entry della tabella) e l’hacker entrerà, autenticato, nel programma.
In presenza di una qualsivoglia tipologia di riconoscimento utente, l’identità che assumerà l’hacker (intendo il nome utente con cui risulterà collegato) dipende da come è strutturato il codice. Se il primo utente di tabella_utenti è l’amministratore, molto probabilmente (3) l’hacker lo impersonificherà.
____
(3) Spesso infatti, presupponendo che solo una sia la riga risultante dall’esito dell’interrogazione SQL, proprio la prima riga della lista viene associata all’utente loggato (hacker).
Autenticazione utente con inganno sullo username
Abbiamo visto come sia possibile ottenere accesso non autorizzato inserendo stringhe (in)opportune nel campo riservato alla password. Lo stesso si può ottenere anche utilizzando il campo username.
Inserendo in username la stringa: qualsiasi_nome’ OR 1 -- si ottiene:
Codice PHP:
SELECT * FROM tabella_utenti WHERE usr=’qualsiasi_nome’ OR 1 -- AND pwd=’qualsiasi_stringa’;
E l’accesso è assicurato. “- - “ rappresenta per MySQL il delimitatore di commento: tutto ciò che segue viene ignorato.
Autenticazione con firma sulla password
Poniamo invece subito all’attenzione una questione fondamentale: se il sistema di autenticazione utilizza un ben più sicuro sistema di firma, per il quale la password non è memorizzata in chiaro su database ed in sua vece è memorizzato il suo hash, il programma sarà non vulnerabile al primo exploit menzionato.
Se infatti confrontiamo l’hash di quanto inserito dall’utente con ciò che è su db (già hashato), il comando md5 vanifica, trasparentemente, gli sforzi dell’hacker.
Se il codice di autenticazione fosse infatti:
Codice PHP:
$utente_autorizzato=0;
$sql=”SELECT * FROM tabella_utenti WHERE usr=’".$_POST[‘username’]."' AND pwd='".md5($_POST[‘password’])."'";
$query=mysql_query($sql,$db);
if ( mysql_num_rows($query) > 0 )
{
$utente_autorizzato=1;
}
la query, anche in presenza di tentativi di hacking, risulterebbe simile a:
Codice PHP:
SELECT * FROM tabella_utenti WHERE usr=’qualsiasi_nome’ AND pwd=’ 6f8f57715090da2632453988d9a1501b’;
e sarebbe, appunto, non vulnerabile.
Ottenere login di amministrazione
Conoscendo la struttura interna delle tabelle del database, è possibile ottenere l’accesso di amministratore, con un po’ di fortuna ed alcuni tentativi. Poniamo che l’applicazione PHP permetta di cambiare la password dell’utente e che usi una query SQL simile alla seguente per farlo:
Codice PHP:
$sql = "UPDATE tabella_utenti SET pwd=’”.$_POST[‘password’].”’ WHERE usr=’”.$username.”’”;
Se come password viene inserita la stringa: mia_pass' WHERE usr LIKE ' %admin% ' -- la query risulta:
Codice PHP:
$sql = "UPDATE tabella_utenti SET pwd='mia_pass' WHERE usr LIKE '%admin%' -- WHERE usr=’marco’”;
Con il risultato che la password di amministrazione è stata modificata in mia_pass, ciò permettendo all’hacker di loggarsi come amministratore – il quale amministratore, per parte sua, sarà estromesso dal sistema!
4.2 GET/POST numerici
Introduzione
Nelle query del tipo seguente, eseguite lato server dall’applicazione PHP:
Codice PHP:
$sql=”SELECT id_documento, user_id FROM tabella_upload WHERE user_id=”.$_GET[‘userid’];
è necessario controllare che la variabile passata in GET (identicamente vale per POST) sia realmente un numero. Pensiamo infatti al caso in cui per varie ragioni (4) siano noti i nomi delle tabelle del database e lo userid passato in query string sia: 60 AND 0 UNION SELECT usr, pwd FROM tabella_utenti.
La query risuta:
codice:
SELECT id_documento, user_id FROM tabella_upload WHERE user_id=60 AND 0
UNION SELECT usr, pwd FROM tabella_utenti
Se a questo punto l’applicazione prevede un output per tali dati, l’hacker ha la lista completa di login e password degli utenti. Vengono estratti solamente i dati soddisfacenti alla seconda SELECT, in quanto la condizione AND 0 rende sempre falsa la prima.
Di qui, se la lista è in chiaro, il disastro è completo; se la password è stata invece hashata con un md5(), ad esempio, può essere possibile che con un attacco di brute force si riesca comunque ad evincere un login valido. E’ bene ribadire che più corta e “debole” (5) è una password, più facile è che l’attacco brute force abbia successo (6).
Di male in peggio: riferendoci sempre alla query sopra, pensiamo cosa può succedere se, in determinati casi, l’hacker attribuisce a user_id la stringa: 60 AND 0; DROP table qualsiasi_tabella.
La query diviene:
codice:
SELECT id_documento, user_id FROM tabella_upload WHERE user_id=60 AND 0; DROP table qualsiasi_tabella
e ci siamo giocati un’intera tabella (R.I.P.). Va detto però che in genere query multiple non possono essere eseguite, non dalla versione 3.0.6 del PHP in poi. A meno che, ovviamente, lo stesso programmatore non preveda tale possibilità tramite codice.
GET/POST numerici con query ad “*”
Nel caso invece di query del tipo seguente:
Codice PHP:
$sql=”SELECT * FROM tabella_upload WHERE user_id=”.$_GET[‘userid’];
poiché UNION implica che il numero di campi estratti dalle due query sia il medesimo, conoscendo la struttura interna della tabella del db o per tentativi ed errori, costruiamo quindi:
codice:
SELECT * FROM tabella_upload WHERE user_id=60 AND 0
UNION SELECT usr, pwd, 0, 0, 0, 0, 0 FROM tabella_utenti
nel caso in cui i campi di tabella_upload siano 7.
Il risultato è quello sperato (dall’hacker, non dall’amministratore dell’applicazione..):
marco e887a115f99cb3a3d01baf63d0ddc682 0 0 0 0 0
monica 7cf706b9c0a54bc05e6ac6baea77d59c 0 0 0 0 0
Rimedio per exploit basati su query con dati numerici
Evitare tali exploit è facilissimo: basta eseguire un controllo sul dato passato e forzarlo ad essere un numero:
Codice PHP:
$sql=”SELECT id_documento, user_id FROM tabella_upload WHERE user_id=”.(int)$_GET[‘userid’];
In caso di tentativo di hackeraggio, al limite il valore della variabile GET/POST sarà nullo e non verrà estratto alcunchè. Se alla nostra query vengono passati dati solo numerici, quindi, è strettamente necessario operare un “type casting” su di essi.
4.3 Evitare ogni tipo di SQL injection
1. Settare la direttiva magic_quotes_gpc del php.ini ad ON.
Tale direttiva equivale ad un addslashes automatico sui caratteri considerati pericolosi relativi a tutte le stringhe passate via GET e POST e su tutto quanto salvato nei cookies.
Fare l’escape significa far precedere al carattere “ ‘ “ ed altri caratteri un backslah, di modo che, riferendoci a quanto visto prima, se inseriamo qualsiasi_stringa' OR 1 nel campo password, questo viene automaticamente convertito in qualsiasi_stringa\' OR 1, vanificando ogni tipo di attacco.
Si deve tener presente però di non arrivare ad aggiungere backslash doppi o tripli, ciò che avviene se il programmatore, abituato a programmare con magic_quotes_gpc ad OFF, fa l’escape “a mano” egli stesso di tutte le stringhe in input: in questo caso risulterebbe qualcosa del tipo: qualsiasi_stringa\\\' OR 1, in quanto l’escape viene fatto sia su “ ‘ “ che su “ \ ”.
Il programmatore deve quindi accertarsi dell’impostazione sulla direttiva del php.ini prima di decidere sul da farsi. Se ad OFF, usare addslashes; se ad ON no.
Per svariate motivazioni, tuttavia, questo non è il metodo migliore.
2. In luogo di magic_quotes o dell’addslashes “manuale”, per fare l'escape dei caratteri pericolosi, è preferibile utilizzare le funzioni apposite che PHP mette a disposizione nell’interazione con MySQL.
E’ opportuno usare mysql_escape_string() sulle variabili passate alle interrogazioni SQL.
Da php.net:
mysql_escape_string aggiunge le sequenze di escape in una stringa per l'uso in mysql_query.
Uso: string mysql_escape_string ( string stringa_senza_escape );
Questa funzione aggiunge le sequenze di escape a stringa_senza_escape, in modo che sia sicuro usarla in mysql_query().
Nota: mysql_escape_string() non aggiunge le sequenze di escape a % ed a _.
Sì, tale funzione (come addslashes) non fa l’escape di tutti i caratteri potenzialmente pericolosi, quali “ % ”, usati nelle query con LIKE, “ ; ” e “ , ”. Per questi è necessario uno str_replace “manuale”.
3. Espressioni regolari.
E’ bene valutare tramite espressioni regolari se quanto si riceve in input è conforme a quanto ci si aspetta di ricevere. Ad esempio, si intendiamo che una variabile contenga solo caratteri alfanumerici possiamo usare la regexp: ereg("^[A-Z0-9]+$",$in) per validarla.
Le espressioni regolari, se ben usate, sono un metodo sicuro per implementare un’applicazione non vulnerabile alle SQL injections.
4. per i dati numerici, forzarli sempre ad essere effettivamente numerici all’atto del passaggio ad una interrogazione SQL tramite type casting: (int)$dato_numerico.
____
(4) Poiché, per nostra fortuna, PHP non dà indicazioni sui nomi delle tabelle, né durante il suo normale funzionamento né in caso di errore, questo tipo di attacco è effettivamente efficace se i nomi delle tabelle sono i soliti noti nomi banali oppure se l’applicazione è open source. Oppure ancora se riusciamo ad ottenerli per altra via, sfruttando altri exploit esistenti.
(5) Per debole si intende un password composta di caratteri, magari solamente minuscoli, o numeri: [a-z]+[0-9], oppure addirittura composta di una parola di senso compiuto ricavabile da un comune dizionario.
(6) Non dò qui menzione delle collisioni relative all’MD5. Se (giustamente) paranoici, usare l’algoritmo SHA oppure una qualche combinazione più sicura dell’MD5,quale, ad esempio: md5(md5($password.$stringa_particolare)).