The lab
here is how portswigger describes it
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.
this follows the reflected xss delivery lab. every earlier lab in the series smuggled over http/1. this one is different in two ways, the front end speaks http/2 to us, and instead of poisoning what a victim receives we poison the response queue itself so that a response meant for the admin comes back to us. that captured response contains the admin's session cookie, and with it we log in as them
what h2.te is and why the downgrade matters?#
http/2 was supposed to end request smuggling. in http/2 the length of every message is built into the protocol as an exact, unambiguous value, so there is no Content-Length versus Transfer-Encoding argument to have. the catch is that many front ends still talk plain http/1 to their back end, so they take our http/2 request and rewrite it as an http/1 request before forwarding it. that step is called downgrading.
the bug here is that when the front end downgrades our request, it carries across a Transfer-Encoding: chunked header that we included in the http/2 message, instead of stripping it. the front end itself trusted http/2's built in length and thought the request was fine, but the moment it rewrites the request as http/1 and hands it to the back end, the back end sees that chunked header and obeys it. so we are back to a classic te desync, the front end used the http/2 length and the back end uses chunked. that pattern is named h2.te, http/2 on the front, transfer encoding on the back.
Step 1 - Confirm the smuggle#
we send a request to repeater, expand the inspector's request attributes section, make sure the protocol is set to http/2, and send this.
POST / HTTP/2
Host: your-lab-id.web-security-academy.net
Transfer-Encoding: chunked
0
SMUGGLED

the front end forwards this by http/2 length, the back end reads the chunked body, sees the 0 chunk, ends the request, and leaves SMUGGLED as the start of the next request on the connection. we send a second request and it comes back as a 404, because our SMUGGLED prefix got glued to the front of it and made a broken path. that 404 confirms the back end is appending later requests to our smuggled bytes, so the desync works.
Step 2 - Smuggle a complete request to poison the queue#
confirming the smuggle is not the goal here, poisoning the response queue is, and that needs a subtly different payload, a complete smuggled request rather than a dangling prefix.
POST /x HTTP/2
Host: your-lab-id.web-security-academy.net
Transfer-Encoding: chunked
0
GET /x HTTP/1.1
Host: your-lab-id.web-security-academy.net

notice both paths point at /x, a route that does not exist, so every response involved is a 404. that is deliberate, it makes the admin's response, when we finally catch it, stand out immediately because it will not be a 404 like everything of ours.
here is why a complete smuggled request poisons the queue. the front end sees one request and expects one response. the back end sees two, our outer post and the complete smuggled GET /x, so it produces two responses. the front end passes the first one back to us and the second response has no request waiting to claim it, so it sits in the connection's response queue. from that point the whole queue is shifted by one, every response on this connection now belongs to the request before it, not the one that asked. the queue is poisoned.
Step 3 - Capture the admin's login response#
now we just fish. we send our request again and wait about 5 seconds between tries. most of the time we get our own 404 back, which is the shifted queue handing us a stale response. but the connection is shared with other users, and the lab has an admin who logs in every 15 seconds or so. when the admin's login request travels over this poisoned connection, the offset means the admin's real login response, a 302 redirect carrying their freshly issued session cookie in a Set-Cookie header, gets served to us instead of to them. so we keep sending until a response comes back that is not a 404, and a 302 with a session cookie is the admin's login landing in our lap.

Step 4 - Use the stolen session and delete carlos#
we copy the admin's session cookie out of that captured 302 and send a request as them, using their cookie, to reach the admin panel.

then we hit the delete endpoint for carlos with that same admin session.

the request goes through as the admin, and carlos is deleted.
with this, we solved the lab!
how the whole attack worked?#
let me pull the threads together. the underlying technology is http desync, the same root as the whole series, but reached through an http/2 downgrade rather than a raw http/1 trick. the front end trusted http/2's clean built in length and never realised our request was ambiguous, but by keeping our Transfer-Encoding header when it rewrote the request into http/1, it handed the back end a request the back end would measure differently. that split is the h2.te vulnerability.
poisoning, in this lab, is about the responses rather than the requests. ordinary request smuggling prepends bytes to the next person's request, poisoning what the victim sends or receives. response queue poisoning goes one level up, by smuggling a whole extra request we make the back end emit one more response than the front end expects, and that surplus response permanently offsets the pairing of requests to responses on the connection. every response now goes to the wrong request.
and that is exactly how we stole the admin session. we did not attack the admin directly, we corrupted the shared connection so that responses no longer matched their requests. when the admin logged in, the server built a perfectly normal 302 response with a brand new session cookie for them, but the offset queue delivered that response to us on our next poll instead of to the admin. we read their Set-Cookie straight out of the captured response, presented that session as our own, and the application could not tell us apart from the real admin, so it let us into the panel to delete carlos.
the fix is to not downgrade at all, the front end should speak http/2 to the back end end to end, which keeps the unambiguous length and removes the desync. if downgrading is unavoidable, the front end must sanitise the request during rewriting, in particular stripping or rejecting a Transfer-Encoding header rather than passing it through, so an http/2 request can never smuggle a length ambiguity into the http/1 hop
