Skip to content

Instantly share code, notes, and snippets.

@excile1
Last active April 9, 2025 13:27
Show Gist options
  • Select an option

  • Save excile1/556a088cccf294f91cdb2d98bad6fb75 to your computer and use it in GitHub Desktop.

Select an option

Save excile1/556a088cccf294f91cdb2d98bad6fb75 to your computer and use it in GitHub Desktop.
Intigriti Challenge 0125 - Writeup by excile

Introduction

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--

Checking out the website

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:

image

image

image

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..

Diving into the JavaScript

Now, there is a function that is called checkQueryParam(), which decides whether to show the modal based on two factors:

  • GET parameter text is not empty
  • XSS() function has to return false

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!

Banging my head against the wall

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 &lt; and &gt; instead of <, >, and hence I was seeing <script>alert(1);</script> instead of the name:

image

image

(but most of the time I'd still get thrown out due to the filter triggering)

A shame once again.. :(

Diving into the JavaScript 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.search is going to return everything in the URL after ? (since everything after that is considered to be a part of GET parameters), and window.location.hash is 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:

  1. It parses the parameter from window.location.href, which, when compared to window.location.search and window.location.hash, contains the whole URL.
  2. 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)

image

image

  1. 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 new GET parameter), or until the next # (start of the window.location.hash).

image

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!

Involving some path trickery

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.search and window.location.hash are empty! Hey, and the text parameter is being parsed by the RegEx correctly

image

image

  • and then there's the bad: Since we are not using a ? character, the server no longer treats our 'fake' text as a GET parameter, it tries the next best thing -- looking for a route on the website with our parameter present, hence, we're getting a 404 :(

image

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!!

image

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?

image

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

image

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:

image

image

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment