PortSwiggerlabs: Response queue poisoning via H2.TE request smuggling
Information :
This lab is vulnerable to request smuggling because the front-end server downgrades HTTP/2 requests even if they have an ambiguous length.
To solve the lab, delete the user carlos by using response queue poisoning to break into the admin panel at /admin. An admin user will log in approximately every 15 seconds.
The connection to the back-end is reset every 10 requests, so don’t worry if you get it into a bad state - just send a few normal requests to get a fresh connection.
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:
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 :
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
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 .
We get a new error , this time it didn’t accept both of them at the same time .
This is because even when we downgraded to HTTP/1.1 it still used HTTP/2 to communicate with the backend — so the classic approach won’t work here. This is a different variant called H2.TE.
Quick note on H2.TE:
The setup here is slightly different, the front-end accepts HTTP/2 but downgrades to HTTP/1.1 when talking to the back-end. Normally HTTP/2 doesn’t care about Transfer-Encoding since it has its own binary framing, but if you manually inject a TE: chunked header inside the HTTP/2 request, the front-end just ignores it but when it downgrades, that header comes along for the ride and the back-end sees it and starts parsing the body as chunked. From there it’s the same idea, leftover sits in the buffer, next request gets poisoned.
Now back to the lab :
One thing i tried is to obfuscate the header , that way it might give us a 200 instead :
And it worked .
But for this lab we don’t need to go through all of that — since the back-end follows RFC 7230 , it will always prefer Transfer-Encoding over Content-Length when both are present , so we only need to inject the TE header alone and don’t need to bother with Content-Length at all . And since this is H2.TE , the front-end speaks HTTP/2 so it doesn’t even look at the TE header in the first place, no obfuscation needed , no risk of triggering that error . The HTTP/2 framing handles the length for the front-end , and the injected TE header takes care of the back-end after the downgrade .
1
2
3
4
5
6
7
POST / HTTP/1.1
Host: 0a84002804ef05af81241bac00930033.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: xchunked
0
Now why are we keeping the Trasnfer encoding in particular , here we’re expecting that if it has to downgrade to HTTP/1.1 , it will copy the transfer Encoding header when rewriting the HTTP request . And if the backend follows the RFC (which should be the case) it will see the transfer encoding header and prefer it over the CL .
And this is exactly how we get our HTTP Request Smuggling , by having a front end that uses CL with HTTP2 and a backend that uses Transfer encoding via HTTP/1.1 .
And just like with a normal CL.TE , we termminate the request with a terminating character , from there we poison the buffer with a smuggled request :
1
2
3
4
5
6
7
8
9
POST / HTTP/2
Host: 0a84002804ef05af81241bac00930033.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
0
GET /404_Return HTTP/1.1
X-Ignore: x
- Here we’re keeping the HTTP2 since it’s an H2.CE .
- Make sure you don’t add a return line after the x since that’s where we’re appedning the normal request .
- Keep Update Content Length disabled .
Perfect , now if we send a normal request :
We got our 404 which means the request was smuggled .
Now this lab is a bit different , we’re trying to poison the Queue for the different responses , we can do that by smuggling a complete GET request instead of what we’re used to smuggling .
So it will look something like this : GET Request –> Complete Smuggled GET Request –> Normal Request + Smuggled Request :
Now this lab is a bit different , we’re trying to poison the response queue . We can do that by smuggling a complete GET request instead of just a partial prefix like we’re used to . The back-end ends up processing two requests but the front-end only knows about one , so it gets two responses back , takes the first one , and the second just sits in the queue . The next user’s request then picks up that leftover response instead of their own , and their response might end up being ours . That’s the poison.
So to compare it to what we usually do when we smuggle a partial request and append the next one to :
With a partial prefix you’re corrupting the victim’s request, your leftover gets prepended to theirs and corruptes it. With a complete smuggled request you’re not touching their request at all , you’re just hijacking their response by leaving an extra one sitting in the queue :
- Partial prefix → you’re corrupting the victim’s request
- Complete request → you’re hijacking the victim’s response without even touching their request
Now following the RFC to have a complete GET request , we should end it with a return line :
1
2
3
4
5
6
7
8
9
10
POST /AAAAA HTTP/2
Host: 0a84002804ef05af81241bac00930033.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
0
GET /404_Return HTTP/1.1
Host: 0a84002804ef05af81241bac00930033.web-security-academy.net
Why are we requesting endpoints that won’t exist , this is so that we know the difference between our Response and the Response of another user .
We’re expecting a 404 , the user isn’t , so if we get his response , we get the 200 or 302 or whatever , and he gets our 404 :
Now we keep sending this Request until we get a different response than the 404 :
We get a 200 which means the attack is working , we are able to capture other user’s responses :
Either do it manually , or just use Intruder :
- Use Sniper Attack .
- Use Null Payloads .
- Continue Indefinetely .
- We must remove the Update Content-Length as well :
We are able to get a 302 instead of the 404 :
We got a cookie as well . Now all we need to do is use Devtools to login as the admin and Delete Carlos :
Once we delete the user , we should solve our lab .
That was all for this lab , see you in the next one .