HTTP request smuggling, confirming a CL.TE vulnerability via differential responses

5 min read Easy PortSwigger
HTTP request smuggling
Contents

On this page

The lab

here is how portswigger describes it

This lab involves a front-end and back-end server, and the front-end server doesn't support chunked encoding.

To solve the lab, smuggle a request to the back-end server, so that a subsequent request for / (the web root) triggers a 404 Not Found response.

it is a different kind of bug from everything before it, there is no input field to poke and no payload reflected on a page, the whole attack lives in the raw shape of the http request and a disagreement between two servers about where one request ends and the next begins

What http request smuggling is?#

most real sites are not one server. there is usually a front end, reverse proxy or load balancer that faces the internet, and behind it a back end that does the real work. the front end takes each incoming request, decides where it ends, and forwards it on to the back end, often reusing one connection to pass many requests through in a row

for that to work, both servers have to agree on exactly where each request stops and the next one starts. a request has a start line and headers, and then optionally a body, and the length of that body is what tells a server when the request is finished. there are two ways to state that length. one is the Content-Length header, which gives the body size as a number of bytes. the other is Transfer-Encoding: chunked, which says the body arrives in labelled chunks and ends with a chunk of size zero

request smuggling happens when the front end and the back end use different rules to measure the body. if we craft a request that puts both a Content-Length and a Transfer-Encoding header on it, and the two servers each trust a different one, they will disagree about where the request ends. the front end forwards what it thinks is one request, but the back end reads less of it, and the leftover bytes sit at the front of the connection and get glued onto the start of whoever's request comes next. that leftover piece is the smuggled request, and it lets us prepend our own content to another person's request

What a cl.te vulnerability?#

the disagreements are named after which header each server obeys. cl is Content-Length, te is Transfer-Encoding. cl.te means the front end uses the Content-Length and the back end uses the Transfer-Encoding. that is exactly this lab, the description even tells us the front end does not support chunked encoding, so the front end falls back to counting bytes with Content-Length while the back end honours the chunked encoding.

so the plan writes itself. we send a request with both headers. the front end reads Content-Length and forwards the whole body. the back end reads Transfer-Encoding: chunked, sees the zero sized chunk that marks the end, stops there, and treats everything after that zero chunk as the beginning of a brand new request. whatever we placed after the zero chunk is smuggled.

Step 1 - Set repeater up for raw http#

this lab is served over http/2 and smuggling is an http/1 problem, so we need to send a real http/1 request. we intercept request, send it to repeater, and in the inspector switch the protocol to http/1, so burp will send our exact bytes rather than an http/2 message. it also helps to turn off the automatic content length update so burp does not correct the number we set on purpose.

Step 2 - Send the smuggle request#

here is the request.

HTTP
POST / HTTP/1.1
Host: your-lab-id.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Transfer-Encoding: chunked

0

GET /404 HTTP/1.1
X-Ignore: X
1

How this request works?#

it carries both length headers, Content-Length: 35 and Transfer-Encoding: chunked, which is the setup for a cl.te disagreement. the body, everything after the blank line, is this.

HTTP
0

GET /404 HTTP/1.1
X-Ignore: X

that body is exactly 35 bytes, which is what Content-Length: 35 claims. so the front end, counting by Content-Length, reads all 35 bytes as the body of our post and forwards the entire thing to the back end as one request.

the back end reads the same bytes but obeys Transfer-Encoding: chunked. in chunked encoding the body is a series of chunks, each introduced by its size in hexadecimal, and a chunk of size 0 means the body is over. our body begins with 0 followed by a blank line, so the back end reads that zero chunk, decides the request body ends right there, and considers this request complete after the 0.

but the front end already forwarded more than that. the bytes after the zero chunk, GET /404 HTTP/1.1 and the X-Ignore: X header, are still sitting in the connection. the back end has no idea they belonged to our body, so it treats them as the start of the next request on that connection. that is the smuggled request, a GET /404 left waiting at the front of the queue.

the X-Ignore header at the end is a small piece of housekeeping. when the next real request arrives, its request line gets appended right after our smuggled bytes, and we do not want it landing in a way that breaks the smuggled request. by ending on X-Ignore: X with no finished value, the next request's first line is swallowed as the value of that header, so our GET /404 stays intact as the request that actually runs.

Step 3 - Send it again and read the differential response#

the first send plants the smuggled GET /404 at the front of the back end connection. now we send the very same request a second time.

2

this time our request arrives to find the smuggled GET /404 sitting in front of it, so the back end serves the /404 first and answers with a 404 Not Found. we asked for a normal page and got a 404, which is not what that request would ever return on its own.

that difference is the confirmation. the response we got does not match the request we sent, which can only happen if a previous request left something behind in the connection, and that is request smuggling proven. this is what confirming via differential responses means, we do not need to see any secret data, the mere fact that the response changed based on a request we sent earlier is proof the two servers parse the boundary differently.

3

with this, we solved the lab!