Exploiting Reflected Input Via the Range Header
TL;DR
Reflected input is often unexploitable because the attack ends up in a place which stops it working, such as inside a quoted attribute. However, the Range header can be used to force the server to send only the attack section from the document, making it fully-exploitable in the process.
Background
In recent months I’ve been doing quite a lot of research around improving my ability to detect desync and header injection vectors, and then make them fully exploitable. And as part of this, even though they are generally quite effective on their own, I also wanted to find better ways to insert XSS attacks into the responses which were sent back to the victims. Because, after all, there’s nothing quite like a bit of instant gratification to warm even the blackest of hearts, right?
Now, as I’m sure you know, one of the pleasing things about desync and header injection is that unlike anything delivered through a browser, you aren’t restricted by the limitations of CORS and fetch. Which means that you can pretty much put whatever you like into the requests.
As part of this, I allocated some time to looking at the Range header, and noticed a few useful things:
all of the main browsers will happily accept an unsolicited Range response (206 Partial Content). Yay!
and something like 30-40% of all endpoints will respond to a Range request
In the Red Corner
From an offensive perspective, this should be interesting as it lets you take an unexploitable vector, glue it together with desync or header-injection, and make it fully exploitable.
There are two bits you’re looking for. The first is endpoints with reflected input which ends up being incorporated into the response verbatim, but alas somewhere unexploitable (like inside quotes in an attribute). Then the second bit is whether the same endpoint will respond to the Range header.
To test for the latter, just add the following header to the request and look for a 206 Partial Content response:
Range: bytes=0-0
Putting it all Together
In practice (once you’ve found a target that meets all the requirements), the actual delivery ends up being straightforward.
Just remember to also strip the Accept-Encoding header from the request, so you get back the raw document, not an unusable chunk of binary noise.
GET /api/v1/web-cookie-privacy/config?locale=en&appId=(console.log(`XSS`));//&theme=default&tea=1 HTTP/1.1
Host: www.tiktok.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Cache-Control: no-cache
Connection: keep-alive
Range: bytes=80-99
HTTP/1.1 206 Partial Content
Content-Type: application/json; charset=utf-8
Server: TLB
Content-Range: bytes 80-99/105
Content-Length: 20
(console.log(`XSS`))
(no tiktok streamers were harmed in the making of this blog)
In the Blue Corner
This is actually pretty tough to defend against, as all the individual steps in the attack chain are relatively benign. Unexploitable reflected content, and an endpoint that responds to the Range header, don’t even warrant an informational note in a report.
The only way to spot this is to understand how the individual issues can be assembled into a working attack chain, and then keep an eye out for the combinations.
Additional Information
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
“Thanks”
I just wanted to say a big “thanks” to my inability to say no to intrusive thoughts. It’s the little things, right?