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 parses

1
2
Content-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