The lab
here is how portswigger describes it
This lab is vulnerable to request smuggling because the front-end server downgrades HTTP/2 requests and fails to adequately sanitize incoming headers. the fix is to sanitise headers during the downgrade, the front end must strip or reject any
\r\ninside a header value so it can never split a request when rewritten to http/1. more broadly, not downgrading at all and speaking http/2 to the back end end to end removes the entire class of downgrade based desync. 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 10 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 crlf injection lab. there we used a \r\n in a header value to smuggle a Transfer-Encoding header past the downgrade. this lab uses the same \r\n injection but for a different end, we split our one request into two complete requests, which poisons the response queue and lets us capture the admin's login response, exactly the goal of the earlier h2.te poisoning lab reached by a new route.
splitting vs smuggling#
it helps to be clear about the difference from the last lab. smuggling leaves an incomplete prefix on the connection that waits for the next request to finish it. splitting is cleaner, we inject enough carriage return and line feed characters to end our first request entirely and write out a whole second, complete request after it. one request goes in, two come out, and because the second one is complete it gets its own response with nothing waiting to claim it, which is precisely what poisons the response queue.
the reason we can do this over http/2 is the same as the last lab. in http/2 a header value is binary data and a \r\n inside it means nothing structural, so the front end passes it through without alarm. only when the front end downgrades our request to http/1 text does the \r\n start ending lines, and a run of them, \r\n\r\n, ends the header block completely, which is how our injected second request becomes a standalone request.
How the split completes itself?#
there is one detail that makes this work neatly. when the front end finishes writing out the downgraded request, it appends its own \r\n\r\n to mark the end of the headers, as every http/1 request must. we take advantage of that. we inject a \r\n\r\n to close our own request, then write the start of a second request, and let the front end's automatically appended \r\n\r\n serve as the terminator for that second request. so the trailing sequence the front end adds during downgrading is what turns our smuggled prefix into a complete request, no guesswork on our part.
Step 1 - Point the request at dead endpoint#
we intercept a request, send it to repeater, and change the path to an endpoint that does not exist.

both this request and the one we smuggle will point at dead paths, so every response of ours is a 404. that is on purpose, it means when we finally capture the admin's response it will not be a 404, so it stands out at a glance.
Step 2 - Inject crlf split into a header#
using the inspector we append an arbitrary header, and into its value we inject \r\n sequences that end our request and lay out a second, complete request to another non existent endpoint.
foo: bar\r\n\r\nGET /x HTTP/1.1\r\nHost: your-lab-id.web-security-academy.net

reading that value, bar finishes the foo header, then \r\n\r\n ends the whole header block of our first request, and GET /x HTTP/1.1 with its Host begins a second request. when the front end downgrades this and appends its own \r\n\r\n, that second request is completed, so the back end receives two full requests where the front end thinks it sent one.
Step 3 - Send and poison the response queue#
we send the request a few times.

because the back end answered two requests while the front end only expected one response, the extra response has no request to go to and sits in the connection's queue. from then on every response on this connection is shifted by one, it belongs to the request before it rather than the one that asked. the queue is poisoned. we keep sending, and each time we mostly get our own 404 back from the offset queue.
Step 4 - Capture the admin login and take over#
the connection is shared, and an admin logs in every 10 seconds or so. when the admin's login request travels over this poisoned connection, the offset delivers the admin's real login response to us instead of to them. that response is a 302 redirect carrying the admin's freshly issued session cookie in a Set-Cookie header. we repeat until a response that is not a 404 comes back, and a 302 with a session cookie is the admin's login landing in our hands.

we copy that session cookie and send a request as the admin to reach the admin panel.

Step 5 - Delete carlos#
with the admin session we hit the delete endpoint for carlos.

the request runs as the admin and carlos is deleted.
with this, we solved the lab!
the whole attack rests on the front end trusting our headers through a protocol downgrade. in http/2 our injected \r\n characters were invisible as structure, so the front end saw one ordinary header and forwarded the request, but when it rewrote everything into http/1 text those characters became line breaks, splitting our single request into two complete ones. that surplus request produced a surplus response, which offset the whole response queue so that responses no longer matched their requests. we did not attack the admin at all, we corrupted the shared connection so that when the admin logged in, the server's normal 302 response with their new session cookie was handed to us, and we simply reused it to become them.
