This blog is based on the research by James Kettle from PortSwigger that can be found here and here.
A client-side desync, a.k.a CSD, is an attack in which the victim's web browser is tricked into desynchronizing its connection to the vulnerable website. This is different from regular request smuggling attacks, which cause the connection between a front-end server and a back-end server to desynchronize.
Standard desync/request smuggling attacks depend on specially crafted requests that common browsers will never send. As a result, only websites with a front-end/back-end architecture are vulnerable to these attacks. However, utilizing completely browser-compatible HTTP/1.1 requests can result in a desync. This creates new opportunities for server-side request smuggling attacks and makes client-side desync attacks a whole new class of threat.
Sometimes it is advised for web servers to respond to POST requests without examining the body. A client-side desync vulnerability arises if they subsequently permit the browser to use the same connection for additional requests. In some cases, it is possible to convince servers to ignore the Content-Length header, meaning they will think each request ends at the end of the headers. This is equivalent to setting the Content-Length to 0.
The Client-side desync attack is possible if the back-end server displays the above-mentioned behavior while the front-end continues to rely on the Content-Length header to detect when a request has been completed.
Hunting for Client-Side Desync in the Wild
Finding or creating a request that causes the server to ignore the Content-Length header is the first step in testing for client-side desync attacks. Sending a request with a Content-Length that is longer than the actual content is the easiest approach to test for this attack.
For the practical demonstration, we will be using Portswigger labs. Follow the below-mentioned steps to get started:
-
Capture the request using Burp Suite or other proxy tools to the GET/endpoint.
-
Notice the / endpoint redirects to the /en endpoint by default.
-
Send a request to the repeater tab, and from settings, disable the Update Content-Length option.
-
Change the request method to POST and update the content length to a value greater than 1.
-
Leave the request body empty and send the request like the below request:
-
Observe how the server replies immediately without waiting until the body arrives. This indicates that the application's backend is ignoring the Content-Length header.
Confirming the Client-Side Desync Vulnerability
We have verified that the server is ignoring the content-length header; now it’s time to confirm if the application is vulnerable to a CSD attack. Follow the steps to get started.
-
Send the same GET request to the second tab of the repeater.
-
Re-enable the Update Content-Length option from settings.
-
Click on the + icon and select the option: Create tab group.
-
Create a group by adding both tabs.
-
In the first tab of a POST request, add the arbitrary prefix to the request body:
GET /404 HTTP/1.1
Foo: x
-
Update the connection header as Connection: Keep-alive
-
Change the Send option to Send Group (single connection).
[Note: Repeater's Send group in sequence feature lets you send a series of grouped HTTP requests with a single click.]
-
Send the request and navigate to the second tab and observe that the response received is “Not found,” which is from the second request in the body. This confirms the desync attack.
Testing in the Browser
We have successfully confirmed the CSD attack via Burp Suite (proxy tool); test the same in the browser.
-
Open the separate browser and navigate to the exploit server of the Portswigger lab.
-
Open the developer tools of the browser and navigate to the Network tab.
-
Clear the existing log entries and enable the Preserve log option.
-
Navigate to the Console tab and replicate the attack using fetch() API:
fetch('https://YOUR-LAB-ID.web-security-academy.net', {
method: 'POST',
body: 'GET /hopefully404 HTTP/1.1\r\nFoo: x',
mode: 'cors',
credentials: 'include',
}).catch(() => {
fetch('https://YOUR-LAB-ID.web-security-academy.net', {
mode: 'no-cors',
credentials: 'include'
})
})
Client-side desyncs are commonly caused by requests to endpoints that result in server-level redirects. This poses a little challenge when developing an exploit because browsers will follow this redirect, disrupting the attack sequence.
CORS error is intentionally triggered to prevent the browser from following the redirect by setting the mode: cors. The catch() method is used to continue the attack sequence.
-
Navigate to the Network tab and observe the requests:
-
A triggered CORS error from the main request.
-
404 response to the home page request.
-
Exploiting the Client-Side Desync
The vulnerable application has a comment section reflecting the user's input. By exploiting the desync attack, we can fetch the other user's request containing the session token.
-
Navigate to the comment section.
-
Comment on the post while capturing the request in Burpsuite.
-
Note the CSRF token, Cookie, and PostID from the request.
-
To capture your arbitrary request in a comment, paste the following in the first tab of the repeater.
POST / HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Connection: keep-alive
Content-Length: CORRECT
POST /en/post/comment HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=YOUR-SESSION-COOKIE; _lab_analytics=YOUR-LAB-COOKIE
Content-Length: NUMBER-OF-BYTES-TO-CAPTURE
Content-Type: x-www-form-urlencoded
Connection: keep-alive
csrf=YOUR-CSRF-TOKEN&postId=YOUR-POST-ID&name=wiener&email=wiener@web-security-academy.net&website=https://ginandjuice.shop&comment=
-
In the second tab of the repeater, paste the following request.
Keep in mind that the number of bytes you attempt to capture must be greater than the length of the body of your POST /en/post/comment request prefix but less than the length of the follow-up request.
-
Navigate to the comment section and refresh the browser; observe that the output of GET /capture-me has successfully started.
-
For the final exploit, navigate to the exploit server of the Portswigger lab.
-
Insert the script within the <script> tags in the body panel.
-
Update the content-length number more than that used previously. Store the exploit and click on deliver to the victim.
-
Observe that you will get more responses after updating the content-length header of the vulnerable request.
-
Keep updating the content length until you get the full response.
Final payload:
-
We have successfully retrieved the victim's session token after updating the correct content length.
This is how one can attempt to Identify, Validate and Exploit a Client-Side Desync attack. There could be more attack vectors, but it is essential to understand the mitigations we will discuss in the below section.
Remediations:
Below are a few best practices you should implement to prevent such attacks.
-
Requests with both Transfer-Encoding and Content-Length that do not match the payload size should be blocked.
-
Implement strict controls on the server, so it never assumes requests won't have a body. This is the root of client-side desync vulnerabilities.
-
Utilize two different values to prevent duplicate headers.
-
To improve back-end communication, use HTTP/2. Using a framed structure, HTTP/2 eliminates ambiguity about where HTTP packets stop. Both the Content-Length and Transfer-Encoding headers are no longer required with HTTP/2; in addition, HTTP/2 explicitly restricts the usage of chunked transfer encoding.