Initial Discovery
During a recent engagement I identified an open redirect where a GET parameter would be reflected as-is in the HTTP response Location header without any kind of sanitization. Something similar to this:
Trying multiple kinds of injections, I discovered that newlines and carriage returns characters could be inserted, leading to header injection:
Even more interesting, we can inject arbitrary content in the HTTP response body by inserting two newline characters, leading to reflected cross-site scripting:
However, modern browsers (Google Chrome, Internet Explorer, Firefox) do not interpret the HTTP response body if the HTTP response status code is a 302, so our cross-site scripting payload is useless. Time to find a bypass !
Prior Work
By searching for prior bypasses, I stumbled upon this blog post where Fortinet describes how they bypassed the execution block by setting the Location header to a URI starting with 'mailto://'. Bugcrowd forums also provides some insight into bypasses that may have worked in the past. And this excellent HackerOne report on XSS affecting Twitter, where they used a Location header starting with '//x:1/' definitely sent me in the right direction.
Let's Fuzz
Given that none of the already documented bypasses worked, I decided to write a dumb fuzzer that would generate a list of URLs and open them with xdg-open. To do so, I downloaded the IANA URI schemes list and generated a list of URLs following this format: http://acme.corp/?redir=[URI_SCHEME]://gremwell.com%0A%0A[XSS_PAYLOAD]. Google Chrome and Firefox were tested in this way, Internet Explorer was also tested but with a PowerShell script rather than simply calling xdg-open.
I then spent quite some time closing browser tabs, hoping to be greeted with an alert box :)
A Valid Candidate
Two candidates out of the full IANA URI scheme list worked, and only on Firefox:
- ws:// (WebSocket)
- wss:// (Secure WebSocket).
It simply looks like this:
Opening the link in the latest version of Firefox (version 81 at the time of writing) and we see we are executing JavaScript under the right domain, without being redirected:
Proof-of-Concept
If you want to test this at home, you can download the 302_server script. It will launch a Python3 HTTP server on port 8000, mimicking the behavior I just described.
Update - October 1st 2020
Sergey Bobrov just pointed out that using an empty Location header will work to force Google Chrome to execute the payload. Nice find !
Update - October 2nd 2020
Maxim Rupp just pointed out that using an resource:// URI in the Location header will work to force Firefox 81 to execute the payload. Nice find !