DefCon CTF Quals 2020 - HTTP Desync between HAProxy & Gunicorn
DefCon 2020 CTF Quals
A CTF by Order of the Overflow.
The CTF had a web challenge, uploooadit
which I quite liked due to my affection towards the attack of HTTP Desync.
The Challenge
The Flask application (app.py, store.py) given had two endpoints:
1. POST /files/
The endpoint was used to save plain-text files to the blob storage. It took Content-Type: text/plain
and a custom header X-guid
containing an id
, an identifier for the files to fetch them later. A valid request looked like this:
2. GET /files/<guid>
Provided the valid guid
, we get to fetch our saved file with this endpoint:
0. Invalid request
And if we send an invalid request to the invalid endpoint, we reveal the frontend HTTP server being used:
So we have a Frontend Server haproxy 1.9.10
and a backend app is written in Flask
which is served by the Gunicorn WSGI.
After the usual assessment, the simple scenario and code leaves us with only the situation of testing it for HTTP Desync between HAProxy and Gunicorn.
The Desync can only help us in poisoning the sockets of the backend server, But if we assume that there can be a Bot that is hitting the backend server in intervals with the flag in it’s HTTP request, then the whole scenario starts making sense.
But first, let’s get the HTTP Desync working. The piece here by Nathan Davison came in handy.
As it turns out, the combination of HAProxy and Gunicorn is Vulnerable to CL-TE HTTP desync, what we mean by that is, we can send the Content-Length
(CL) and Transfer-Encoding
(TE) together but if we malform the value of Transfer-Encoding
a little bit by pre-pending non-printable character like “\x0b” (vertical tab) or “\x0c” (form feed), HAProxy will ignore the header and give precedence to CL header but when this is passed to Gunicorn it will parse the TE header correctly and give precedence to that, So if we send a Raw Request like following:
As the HAProxy parses1
2Content-Length: 187
Transfer-Encoding:\vchunked
It only considers the CL header and sends the second request as the body to Gunicorn
But when the Gunicorn parses the TE header, It breaks the above raw request in 2 POST requests.
One of which is normal and complete request till the 0
byte chunk of Transfer-Encoding and where the second request is our poison for the TCP socket which has Content-Length: 385
. The Gunicorn will wait for the next HTTP/TCP packet till the length of 385 is reached.
And as we had assumed, what if we had a Flag BOT which submits HTTP request to the backend after some interval, we can steal its request by making it fall after our poison if it does then the raw HTTP request by the BOT will become our Poison HTTP request’s body and will be perfectly stored through POST /files
endpoint for us to steal through GET /files/2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
Let’s check it out :)
The Exploit request with Transfer-Encoding:\vchunked
And we get the the flag 🚩at GET /files/2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
HTTP Desync is quite fun and prevalent considering the modern architecture that web apps are built on these days.
It was a fun challenge. Definitely some rabbit holes followed in previous CTFs on HTTP Desync helped me out in solving this one in minutes.
On another note, I play for UnderDawgs, if you are looking for a team and are a nerd for maths, crypto, and reversing, please hit us up.
「低い可能性はゼロではないことを意味します」
CaptainFreak