The lab
here is how portswigger describes it
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.
this follows the cl.te confirmation lab. there we only had to prove the smuggle worked by turning a page into a 404. now we put it to use. the admin panel is blocked, but the block is enforced by the front end, and a smuggled request goes straight to the back end without the front end ever inspecting it, so smuggling lets us walk right past the control.
The idea#
the front end is the server that enforces the rule, it refuses any request for /admin. but in request smuggling the smuggled portion is never seen by the front end as a request, it is hidden inside the body of an innocent looking post that the front end waves through. the back end is the one that pulls that hidden request out and runs it, and the back end has no such block. so the whole point of this lab is that a control living only on the front end is worthless against a request that reaches the back end by smuggling
the mechanics are the same cl.te setup as the confirmation lab, front end trusts Content-Length, back end trusts Transfer-Encoding, and we hide the real request after a zero chunk. the interesting part here is the back and forth of getting the smuggled request just right, because the back end is fussy about what a valid request looks like
Step 1 - Confirm the front end blocks admin#
first we just try to visit /admin directly and watch it get refused.

the front end returns a block so a normal request is a dead end. everything from here is about reaching /admin through the back end instead.
Step 2 - Smuggle a plain request for admin#
we intercept a request, send it to repeater, switch to http/1 in the inspector, turn off the automatic content length update, and send this.
POST / HTTP/1.1
Host: your-lab-id.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 37
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
X-Ignore: X
this is the same shape as the confirmation lab. the front end reads Content-Length: 37 and forwards the whole body. the back end reads Transfer-Encoding: chunked, sees the 0 chunk, ends the request there, and leaves GET /admin HTTP/1.1 and its X-Ignore header as the smuggled start of the next request. the X-Ignore header is there to soak up the following request's line so our smuggled request stays clean.

we send it a second time so our own request collides with the smuggled one.

this time the smuggled GET /admin actually runs on the back end, but the response says Admin interface only available to local users. so we got past the front end block, the back end saw our admin request, but it has its own rule, admin is only for requests that look like they came from localhost. that is decided by the Host header, so our smuggled request needs Host: localhost.
Step 3 - Add a localhost host header and hit a snag#
we add Host: localhost to the smuggled request and bump Content-Length to match the longer body.
POST / HTTP/1.1
Host: your-lab-id.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 54
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: localhost
X-Ignore: X

now the response complains Duplicate header names are not allowed. the reason is the way our smuggled request joins with the next real request. our smuggled request already carries Host: localhost, and then the following request's own Host header gets appended right after it, so the back end sees two Host headers on one request and refuses it. we need to stop that trailing request from adding a second Host.
Step 4 - Swallow the trailing request with a content length#
the fix is to give the smuggled request its own body using a Content-Length, so that the leftover bytes of the next request, its Host header included, are read as our request's body rather than as extra headers.
POST / HTTP/1.1
Host: your-lab-id.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 116
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
x=

here the smuggled GET /admin declares Content-Length: 10 and starts a body with x=. that tells the back end the smuggled request has a short body still to come, so it reads the next 10 bytes, which are the opening bytes of whatever request arrives next, as our body. those bytes, including that request's Host header, are consumed as body content instead of being parsed as headers, so there is no second Host to clash. the smuggled request is now well formed with a single Host: localhost, and the back end serves us the admin panel.
Step 5 - Delete carlos acc#
now that a smuggled admin request works, we point it at the delete endpoint with carlos as the username, keeping the same body swallowing trick and adjusting Content-Length for the longer request line.
POST / HTTP/1.1
Host: your-lab-id.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 139
Transfer-Encoding: chunked
0
GET /admin/delete?username=carlos HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
x=

we send it twice the smuggled GET /admin/delete?username=carlos runs on the back end as a local admin request, and carlos is deleted.

with this, we solved the lab!
