In my preparation for taking the Burp Certified Professional test, I ran through the majority of the content on Web Security Academy. I have learned a ton of information and I am thankful for such a great learning resource. If you want to understand web application attack surface, how to defend against OWASP vulnerabilities, or if you have an interest in bug bounty, I highly recommend you check it out.
As part of that learning, I learned about a whole new class of vulnerabilities upon which I previously had very little knowledge – HTTP Request Smuggling or HTTP Desync attacks. These vulnerabilities arise when there is a mismatch in handling between the frontend and backend servers. Understanding where a request ends and where the next request starts might not be the same between the different layers of a web application, and this can be especially true for modern apps that take advantage of microservices architectures. It can be very difficult to ensure all of the disparate pieces of an application handle requests consistently and are not vulnerable to a desync.
The goal of this post is to not only help you solve the associated lab on the PortSwigger Web Security Academy site, but to understand the vulnerability, how to construct the payloads, and how to reproduce when testing against different web applications.
Take this request:
GET / HTTP/1.1
Host: victimsite.com
This is just a normal request flowing towards the root page on victimsite.com. Now, consider a load balancer or a CDN is sitting in front of the origin additionally handling the request. Often, these pieces of frontend infrastructure will maintain a persistent connection (example: HTTP 1.1 Keep-alive) to the backend for performance reasons. Rather than having to establish a new TCP connection, establish TLS connection on top, protocol negotiation, etc., the frontend infrastructure will open and maintain a connection for streaming requests to the next hop. This can be very impactful to reducing network overhead and latency.
Now, what if the frontend and backend disagree on the length of a message? What this allows for is the queuing of content that will get prepended to the next incoming requestion. This is the core principal to HTTP Request Smuggling or Desync attacks – a disagreement on the length of the requests enables an attacker to prepend content to the next request. This can allow an attacker to manipulate the headers on the next request causing denial of service or worse.
Now, take this request
POST / HTTP/1.1
Host: victimsite.com
Content-Length: 15
Content-Length: 14userid=scomurrG
Hopefully, some part of the frontend is defending against duplicate headers, but for the sake of argument let’s say the frontend sees the Content-Length of 15 (the entire payload) because it honors the first CL header it encounters, and the backend sees a Content-Length of 14 because it honors the last CL header it encounters. As a result, the entire payload passes through the frontend to the backend, and the backend processes the request (based on the CL:14). Once it services the request, it has a “G” leftover. Since the backend is expecting a stream of data from the frontend, it assumes the G is simply the start of the next request…and it waits for the rest of the content. Whatever request comes in next, the backend is going to prepend the G to that request and try to process.
Next request:
GGET / HTTP/1.1
Host: victimsite.com
And now there’s a problem. GGET is not a valid HTTP verb and processing will fail. Since this type of attack was initially documented in 2005, modern defenses should be stepping in front of and defending against a duplicate Content-Length header.
Since modern WAFs, CDNs, Load Balancers, etc. most likely are already defending against duplicate Content-Length headers, we need an alternative route for specifying the length of a request. If we have an alternative way to specify the length of the content contained within a request, we have the opportunity introduce a mismatch in handling and force a desync. For the sake of this blog post, this is where we introduce the Transfer-Encoding header.
Here’s what a request looks like using Transfer-Encoding rather than Content-Length.
POST / HTTP/1.1
Host: victimsite.com
Transfer-Encoding: chunkedE
userid=scomurr
0
^^ Request has to have a blank line after the 0 (\r\n\r\n). How does this type of request work? With the TE header, the receiving server knows to look in the body of the actual request to establish the length. Here’s a generic way to look at the body of a TE request:
<length of the payload specified in hexadecimal>
<payload>
0
<Empty Line>
Check this out if using Burp – you can highlight the payload withing a request and Inspector will show you the decimal and hexadecimal lengths of the highlighted section:
So in the case of the main payload containing userid=scomurr, the payload is 14 characters long (E in hex) but would be 14 (decimal) if specified as the CL header. This is super handy when manipulating the requests to try and cause the desync.
Now, combining the Content-Length and Transfer-Encoding headers, we have the ability to cause the same type of desync as we did when specifying two CL headers:
POST / HTTP/1.1
Host: victimsite.com
Transfer-Encoding: Chunked
Content-Length: 60
G
^^ The payload ends right after the G. It does not have an empty line afterwards or any spaces. We want the G to be prepended to the next request with nothing in between (GGET or GPOST).
In this case, Content-Length is handled by the frontend. Here are the 6 characters handled by the frontend:
The frontend passed the full payload through. If the backend honors Transfer-Encoding, then the backend will see the payload as a 0 (a POST with no content). Notice the full empty line which terminates the request for a TE payload (\r\n\r\n in Inspector) followed by a “G”. This “G” now starts the next request from the perspective of the backend. It’s queued up and will be prepended to the next request:
For the purposes of this blog, we’re looking for a CL.TE vulnerability which means the frontend honors the Content-Length header, and the backend honors the Transfer-Encoding header. Since we now see how desync happens when both headers are present for a CL.TE vulnerability, this is all we need!
Now, let’s walk through the Lab: HTTP request smuggling, basic CL.TE vulnerability on the Web Security Academy.
Step 1: Open the Burp built in browser. I absolutely love having the Chromium browser built right into Burp. This negates the need to mess with extensions and forcing a browser through the Burp proxy.
Step 2: Paste the URL for the lab specific instance into the Burp browser. Login to the academy, choose the lab, and then hit the “Access the Lab” button. Once you have the site loaded up, you should see traffic flowing through the proxy log within Burp:
Step 3: Since we need to test for the vulnerability, grab the request to / and send to repeater:
Step 4: In Repeater, change the request to a POST and send. Since we’re dealing with requests that have a request body, we need to use an HTTP verb that allows for the passing of content (outside of fat GET which is for a later post).
Since the request returns (200 OK), we might have the option to smuggle at least part of a request. If this hadn’t worked, we would need to find a different request that would allow for CL.TE smuggling. Notice I got rid of the majority of the request headers to simplify for this lab specifically.
Step 5: Add in the Transfer-Encoding header. Modify the body with the hexadecimal length, the terminating 0, and the appropriate \r\n\r\n at the end of the payload:
Step 6: Try appending and additional character (a “G” in this case to solve the lab):
First Send – this sends the full request through the frontend, and the request up to the 0 is processed by the backend. The queue on the backend waits for the rest of the expected second request. Notice the Content-Length (which the frontend is honoring) gets incremented by 1 as the POST body now has 1 additional character:
Second Send – this request is processed by the frontend as an entirely new request. The backend, however, has a single ‘G’ waiting in queue:
As we can see, the G from the first request was queued on the backend and it gets prepended to the incoming request!
Couple of key notes to pay attention to as part of the lab:
- Chunked is case sensitive. For the lab to work, I needed to pass the header Transfer-Encoding: chunked (lowercase ‘c’). Watch for case sensitivity
- Pay special attention to Inspector and watch to make sure you have the appropriate \r\n to terminate intended lines and that you don’t have them where you need to prepend
- Per the HTTP specifications, a web app is supposed to handle having both Content-Length and Transfer-Encoding specified. Per the spec, the Content-Length header is supposed to be ignored – it does not mean the request is malformed and should be rejected. These are still valid requests, however, Transfer-Encoding is supposed to be the header that is always honored. RFC: “If a message is received with both a Transfer-Encoding header field and a Content-Length header field, the latter MUST be ignored.”
- Content-Length in this lab is being handled automatically by Burp. Under the top level menu for Repeater there is the option to disable automatically updating Content-Length. Since we want the frontend to pass the full payload to the backend as a single request, letting Burp do the work this time makes sense:
Happy hunting!