Skip to content

Instantly share code, notes, and snippets.

@boffman
Last active January 12, 2025 01:04
Show Gist options
  • Save boffman/98578150cc87e70a3c53d27b02dfb338 to your computer and use it in GitHub Desktop.
Save boffman/98578150cc87e70a3c53d27b02dfb338 to your computer and use it in GitHub Desktop.
Intigriti 0125 challenge writeup

Writeup for the Intigriti 0125 challenge

Author: boffman

Overview

The https://challenge-0125.intigriti.io/challenge page shows colorful particles, along with an input dialog where one can enter a name:

image

..which is then shown in a greeting modal dialog:

image

The goal is to somehow trigger a XSS vulnerability to show an alert with document.domain.

Analysis

When submitting the name in the first dialog, the redirectToText function is called, which redirects to /challenge?text=<name>:

       // Function to redirect on form submit
        function redirectToText(event) {
            event.preventDefault();
            const inputBox = document.getElementById('inputBox');
            const text = encodeURIComponent(inputBox.value);
            window.location.href = `/challenge?text=${text}`;
        }

So the value of the text query parameter is extracted and shown in the modal dialog.

checkQueryParam() is executed upon loading the page, and it gets the text value, tries to verify that no XSS injections are taking place, and inserts the value in the innerHTML of the modal:

       // 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';
            }
        }

The XSS() function that it calls, checks if any < or > exists in the query parameters or hash (if so, no modal is shown):

       function XSS() {
            return decodeURIComponent(window.location.search).includes('<') || decodeURIComponent(window.location.search).includes('>') || decodeURIComponent(window.location.hash).includes('<') || decodeURIComponent(window.location.hash).includes('>')
        }

..and the getParameterByName function extracts the text value from the URL by using a regular expression:

       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, " "));
        }

So. this should prevent any payload in the query parameters injecting e.g. a <script> tag in the text parameter.

However!

However, the getParameterByName applies the regular expression on window.location.href - i.e. the whole URL, and not just the query parameters or the hash, that are checked in the XSS() function - and this opens up for a potential loop hole.

If we can somehow inject the text payload before the query parameters and hash, then the XSS() function will not detect any malicious < or > characters in it.

Let's examine the regular expression in getParameterByName a bit closer:

            var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)");
            results = regex.exec(url);
            ..
            return decodeURIComponent(results[2].replace(/\+/g, " "));

It searches for ? or &, followed by name (which is "text"), followed by =. Then it captures the value, all characters up until a &, a #, or end of line.

This works well when the text value is among the query parameters, since the query parameter part starts with a ?name=value and any consecutive parameter is appended with &name=value.

But, if we would inject &text=value& in the path of the URL, rather than in the query parameters, it would still make the regular expression match it.

And this is possible by inserting a "fictional directory" in the URL path, that starts with &text= and ends with a &. However, there is no such location at the server, so trying to load that page would yield a HTTP 404 - not found error.

A trick that can be used for that is to append a /.. after the fictional directory to tell the server to go back to the parent directory again (that is, /challenge). This causes the server to ignore the fact that the fictional directory does not exist, since the user has still specified that it should navigate back up to the parent directory anyways.

This needs to be URL encoded though, otherwise the browser will see the /challenge/fictional_directory/.. in the address bar and normalize it to just /challenge before sending the request to the server.

So...

So if an URL like this is crafted:

https://challenge-0125.intigriti.io/challenge/&text=<img src=x onerror="alert(document.domain);">&/..

..but to prevent the browser from normalizing, some URL encoding can be applied to it:

https://challenge-0125.intigriti.io/challenge/&text=%3Cimg+src=x+onerror=%22alert(document.domain);%22%3E&%2f..

This makes the getParameterByName function/regexp match the path part, extract the <img src=x onerror=... payload and insert it into the page, making the modal dialog implementation end up looking like this:

<div id="modal" class="modal" style="display: flex;">
        <div class="modal-content">
            <h2 id="modalText">Welcome, <img src="x" onerror="alert(document.domain);">!</h2>
            <button onclick="closeModal()">Close</button>
        </div>
    </div>

...where the onerror script gets executed and the alert is popping up:

image

Mission accomplished ✅

Thanks to Intigriti for a fun challenge!

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