23 gennaio 2008

SQL injection: cos'è, come funziona

Oggi introdurrò una delle peggiori minacce per la sicurezza dei database, la famigerata SQL injection. Comincerò parlando del suo funzionamento, per proseguire in un prossimo post con le tecniche più efficaci per evitarla.

Scenario abituale: quando su internet compiliamo i campi di un form, l'applicazione web solitamente concatena il nostro input a una stringa SQL. I nostri dati andranno a formare la clausola WHERE, allo scopo di operare su un subset di record del database.

Per esempio, in un form di login tipicamente inseriremo qualcosa del tipo:

username: pippo

password: pluto

L'applicazione ha pronta una stringa contenente il comando SQL da completare coi dati dell'utente e da inviare al database, nella forma:


SQLstring = "SELECT IDutente FROM tab_Utenti WHERE IDutente = '" & username & "' AND pwd = '" & password & "'"

che verrà completata dal nostro input in:

SQLstring = "SELECT IDutente FROM tab_Utenti WHERE IDutente = 'pippo' and pwd = 'pluto'"

La SQL injection è una tecnica che sfrutta questo funzionamento delle applicazioni web (ma non solo) per portare un attacco al database di back-end, mediante l'inserimento di codice apposito - non previsto dal programmatore dell'applicazione.

La SQL injection è quasi sempre associata alle applicazioni web perché queste si basano su un'architettura a tre livelli (client - web o application server - db server): il cosiddetto middle tier usa un pool di connessioni verso il database, per cui i login avvengono a livello di applicazione più che di database (più deboli perché spesso i programmatori gli associano ampi privilegi sugli oggetti del database). In un sistema client/server, invece, il login nell'applicazione è sostanzialmente allineato al login nel database e in buona parte le tecniche che vedremo non si applicano.

Per i prossimi esempi faremo riferimento a SQL Server di Microsoft, come del resto fanno molti altri siti, libri e articoli che trattano l'argomento. Questo non significa che il problema sia circoscritto a questo DBMS: poiché la SQL injection sfrutta una vulnerabilità dell'applicazione, non del database stesso, tutti i DBMS ne sono soggetti. Tuttavia, SQL Server offre ai suoi utilizzatori un dialetto del SQL molto ricco, il Transact-SQL, le cui funzionalità aggiuntive si sono purtroppo dimostrate utili anche per scopi non previsti; noi le useremo per studiare il funzionamento di questo tipo di attacco. Ripeto, tutti i DBMS possono subire una SQL injection, tanto che gli esempi seguenti sono adattabili - con le dovute modifiche - a ogni database.

Riprendiamo l'esempio iniziale del form di login, e immaginiamo di avere un'applicazione web che riceve in input username e password per verificare se l'utente esiste nella tabella tab_Utenti e se ad esso è associata (mediante il testo contenuto nel campo pwd della tabella) la password digitata. Assumiamo inoltre che (1) l'applicazione non effettui alcuna validazione sull'input dell'utente, e (2) che il comando SQL inviato dall'applicazione al database sia generato mediante concatenazione. Riprendiamo la stringa SQL:

SQLstring = "SELECT IDutente FROM tab_Utenti WHERE IDutente = '" & username & "' AND pwd = '" & password & "'"

La nostra applicazione potrebbe autenticare o meno l'utente in base al risultato restituito dal database. Se il risultato della SELECT è una stringa vuota, l'utente è rifiutato; altrimenti, è autenticato. In questo caso, sarà sufficiente inserire in input queste stringhe:

username: ' OR ''=''

password: ' OR ''=''

per inviare al database:

SELECT IDutente FROM tab_Utenti WHERE IDutente = '' OR ''='' AND pwd = '' OR ''=''

La condizione ''='' (un'identità) è sempre verificata, per cui il database non restituirà una stringa vuota, e l'applicazione di conseguenza autenticherà l'utente, pur non conoscendo né un nome utente valido, né la password corrispondente.

Non ci credete? Provate con un qualsiasi db, per esempio Access. Prendete una tabella già popolata, e impostate la selezione sulla chiave primaria per un valore non esistente. Per esempio, una tabella tab_Clienti, la cui chiave primaria IDCliente non contenga il valore 0:

SELECT * FROM tab_Clienti WHERE IDCliente = 0;

Questo comando restituirà un subset vuoto. Se invece aggiungiamo un'identità in coda:

SELECT * FROM tab_Clienti WHERE IDCliente = 0 OR ''='';

il database ci restituirà tutti i record della tabella. Semplice, no?

Immaginiamo ora che l'applicazione effettui qualche tipo di validazione sull'input, per esempio sulla lunghezza della password inserita. In questi casi torna utile il segno di commento dei dialetti SQL: in SQL Server è "--", per cui tutto ciò che segue il segno viene ignorato dal DBMS. Allora, inserendo:

username: ' OR ''='' --

password: boh!

il DBMS riceverà questo comando SQL:

SELECT IDutente FROM tab_Utenti WHERE IDutente = '' OR ''=''

e, di nuovo, l'utente sarà autenticato, a dispetto del controllo sulla password.

In una variante più insidiosa, l'uso dei commenti è associato con la possibilità di concatenare più comandi SQL nella stessa stringa:

username: ' ; DROP TABLE tab_utenti ;--

password: boh!

da cui al DBMS:

SELECT IDutente FROM tab_Utenti WHERE IDutente = '' ; DROP TABLE tab_Utenti ;

e voilà, la tabella degli utenti se n'è andata...

Un'altra tecnica in voga è quella di accodare all'input una
UNION ALL SELECT ... per estrarre i dati da una qualsiasi tabella del database e accodarli ai record restituiti dalla query originale.

Per finire, un accenno a un uso molto sofisticato della SQL injection, in congiunzione a un
buffer overflow. Come tutti i software basati su linguaggi quali il C e il C++, anche i DBMS sono naturalmente soggetti al problema del buffer overflow. Esso si presenta quando il programmatore non verifica che la lunghezza dei dati non ecceda la dimensione del buffer in uso. In una tale eventualità, i dati andranno a sovrascrivere zone dello stack, con risultati imprevedibili per il funzionamento del sistema.

Per questo tipo di attacco non è neppure necessario approfittare di una stringa concatenata, come per gli esempi precedenti. Solitamente si sfruttano funzioni interne al DBMS, di cui sia nota la vulnerabilità a un buffer overflow (per esempio passando mediante SQL injection una stringa opportuna come parametro). Sovrascrivendo lo stack si può ottenere un
denial of service; ma il "meglio" si ottiene preparando la stringa affinché inserisca codice malevolo e sostituisca l'indirizzo di ritorno della funzione chiamante, in modo tale da eseguire il codice inserito. Si potrà in questo modo tentare, per esempio, la scalata ai privilegi di amministratore sulla macchina. Per avere un'idea di quello che si può fare col buffer overflow, è interessante leggere Smashing the stack for fun and profit, dal n. 49 di Phrack.

Concludo qui, anche se le tecniche di SQL injection sono molto varie, e sul web si possono trovare decine di altri esempi, tutti interessanti. In questa sede era importante spiegarne la logica di funzionamento, per potere poi trattare a breve le contromisure da adottare per contrastarle. Alcune probabilmente vi sono già venute in mente, vero?


Nessun commento: