The challenge is available at https://challenge-0125.intigriti.io/challenge
This was the first ever Intigriti challenge I solved (and also the first ever writeup I tried writing :)), but it was actually a ton of fun to figure out! I spent, maybe, a good couple hours banging my head against a wall, but then it came together!
And here's how I did it--
The website is pretty straightforward, there is one input to enter your name, and whatever you enter gets passed in through the GET
parameter text, which is then rendered on the page:
Well, can't hurt to just try entering the obvious <script>alert(1)</script>, right? That doesn't work, in fact, we don't even get the modal pop-up anymore! What a shame..
Now, there is a function that is called checkQueryParam(), which decides whether to show the modal based on two factors:
GETparametertextis not emptyXSS()function has to returnfalse
Great, so what is that XSS() function doing?
function XSS() {
return decodeURIComponent(window.location.search).includes('<') || decodeURIComponent(window.location.search).includes('>') || decodeURIComponent(window.location.hash).includes('<') || decodeURIComponent(window.location.hash).includes('>')
}So, it returns true only if the window.location.search or window.location.hash contains a < or >. Interesting!
The first logical thought that came to my mind was, hey, let's try bypassing this "filter"! I tried encoding the < and > characters in multiple different ways, like, (single, double, triple) url-encoding them, using the fullwidth versions of the characters (following the PayloadsAllTheThings ideas for XSS bypasses: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/XSS%20Injection/1%20-%20XSS%20Filter%20Bypass.md#bypass-using-unicode), but, hey, no luck!
The best I got to was literally just getting < and > instead of <, >, and hence I was seeing <script>alert(1);</script> instead of the name:
(but most of the time I'd still get thrown out due to the filter triggering)
A shame once again.. :(
First of all, I had to ask the question, what exactly is window.location.search & window.location.href:
https://developer.mozilla.org/en-US/docs/Web/API/Location/search
https://developer.mozilla.org/en-US/docs/Web/API/Location/hash
So, put in simpler terms -
window.location.searchis going to return everything in the URL after?(since everything after that is considered to be a part ofGETparameters), andwindow.location.hashis going to return everything after a#(normally used to target certain elements on the page)
At this time, a different interesting function caught my eye - getParameterByName. So, instead of using, let's say URLSearchParams, that is provided by the browser, we have a custom function to parse the parameter? Interesting...
function getParameterByName(name) {
var url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)");
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}There is a couple things to note about this:
- It parses the parameter from
window.location.href, which, when compared towindow.location.searchandwindow.location.hash, contains the whole URL. - It escapes
[and]in the parameter name with two backslashes? (It doesn't seem to be useful for the exploitation of the website, but still something that I found interesting)
- Then, it matches the URL to the following regular expression:
[?&]text(=([^&#]*)|&|#|$), which captures everything starting with a?text=or&text=, all the way to the end of the URL or until the next&character (since it marks the definition of a newGETparameter), or until the next#(start of thewindow.location.hash).
All of this means that we don't necessarily need to set our text as a GET parameter, all we need for it to work is to match the RegEx!
So, following the previous lead, I tried to construct a URL, such that window.location.search and window.location.hash will be empty, but one that will still match the RegEx! Since the function does not check whether we are actually defining any GET parameters and the regular expression accepts BOTH ? and &, what if we just skip the question mark and try defining a parameter immediately with an ampersand?
Following this approach, I ended up visiting the following URL: https://challenge-0125.intigriti.io/challenge&text=test. There's good news and bad news!
- the good: Both
window.location.searchandwindow.location.hashare empty! Hey, and thetextparameter is being parsed by the RegEx correctly
- and then there's the bad: Since we are not using a
?character, the server no longer treats our 'fake'textas aGETparameter, it tries the next best thing -- looking for a route on the website with our parameter present, hence, we're getting a404:(
At this point I lost hope on this lead, so I got a little bit sidetracked and returned to trying more encodings/other leads/etc., which, unsurprisingly, didn't go anywhere and relatively soon I returned on track :)
So, if we can somehow manipulate the path to still make it point to the /challenge path, this could work!
Then I tried combining the payload with the ? and # in a couple of different ways to, maybe, bypass and escape the window.location.search and window.location.hash, which, unfortunately, didn't work..
And then something clicked-- what if we just try to traverse back to the correct path from our payload?
First of all, I tried doing something like: https://challenge-0125.intigriti.io/&text=test/../challenge, which didn't work, because the browser traversed the path for me and went straight to /challenge. Ugh! All of these browser trying to be smart.. ¯_(ツ)_/¯
Well, thankfully, we can bypass this behavior by URL encoding the slashes! So, the payload becomes: https://challenge-0125.intigriti.io/&text=test%2f..%2fchallenge
And, hey! Now it works!!
We are no longer getting a 404 AND our parameter is correctly fetched, AND guess what? Both window.location.search and window.location.hash are empty, which should mean that we are now ready for XSS! :)
Here, I got an imaginary bottle of champagne, was ready to celebrate and finally go to sleep, went to https://challenge-0125.intigriti.io/challenge&text=<script>alert(1)</script>%2f..%2f..%2fchallenge (also notice how I added another %2f.. to cancel the closing </script> tag, since it also contains a / and will be treated as a new 'route' :)) aaaand.. nothing? :C
Even though the <script> tag is injected, and is actually being treated as a script, it is still for some reason not executing?
So, to find out why, I went to consult the JavaScript once again, more specifically - the checkQueryParam() function:
// Function to display modal if 'text' query param exists
function checkQueryParam() {
const text = getParameterByName('text');
if (text && XSS() === false) {
const modal = document.getElementById('modal');
const modalText = document.getElementById('modalText');
modalText.innerHTML = `Welcome, ${text}!`;
textForm.remove()
modal.style.display = 'flex';
}
}So, the HTML is inserted using modalText.innerHTML = 'whatever'. Consulting the documentation, aaaand-
https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
A-ha! Well, them browsers are being too smart once again.. The <script> tag will specifically not execute if it is injected via .innerHTML. So, we need a different way to exploit it! For this purpose, I went to consult PayloadsAllTheThings one last time: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/XSS%20Injection#xss-using-html5-tags, picked the objectively best payload aaaand- jackpot! Exploitable both in the latest version of Firefox and in the latest version of Chrome:
The final payload I ended up with constructing is: https://challenge-0125.intigriti.io/&text=%3Cvideo%3E%3Csource%20onerror=%22javascript:alert(document.domain)%22%3E%2f..%2fchallenge
Thank you for reading the write-up! / me on github / me on intigriti















