Intigriti's January Challenge Writeup
Description
This challenge is a XSS challenge where we have to find xss on a webpage with a unique filtering.
Inital Look
looks like a regular xss challenge where we have to enter a name and it will be shown in a pop up.
Source Code
here is a stripped down version of the code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome 2025!</title>
</head>
<body>
<form id="textForm" onsubmit="redirectToText(event)">
<h1>Enter your name!</h1>
<label for="inputBox"></label>
<input type="text" id="inputBox" name="inputBox" placeholder="Type here...">
<button type="submit">Submit</button>
</form>
<div id="modal" class="modal">
<div class="modal-content">
<h2 id="modalText"></h2>
<button onclick="closeModal()">Close</button>
</div>
</div>
<div id="particles">
</div>
<script>
function XSS() {
return decodeURIComponent(window.location.search).includes('<') ||
decodeURIComponent(window.location.search).includes('>') ||
decodeURIComponent(window.location.hash).includes('<') ||
decodeURIComponent(window.location.hash).includes('>')
}
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, " "));
}
// 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}`;
}
// 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';
}
}
// Function to close the modal
function closeModal() {
location.replace('/challenge')
}
function getRandomColor() {
// just for design
}
function generateFallingParticles() {
// just for design
}
window.onload = function () {
generateFallingParticles();
checkQueryParam();
};
</script>
</body>
</html>
from our first look here our input is getting reflected in innerHTML
only if 2 conditions are met
- output of
getParameterByName('text')
isn’t empty - output of
XSS()
function should be false
Let’s address the elephant in the room: The JavaScript function XSS
checks for the presence of <
or >
in window.location.search
or window.location.hash
. If it detects any angular brackets, it prevents our popup from loading. As of the time I’m writing this blog, it seems impossible to bypass this function—we can’t find a way to include angular brackets in location.hash
or location.search
.
Initially, I thought, “Hmm, maybe ISO-2022-JP
is popular these days, and there might be a character that could replace angular brackets.” But after fuzzing, I couldn’t find any. I then realized how naive I was—if there were any replacement characters for angular brackets, Gareth Hayes would have discovered them long before I did.
But the application uses only UTF-8
it enforces that with the following tag in header <meta charset="UTF-8">
Let’s examine the other condition—how getParameterByName('text')
is parsing user input. It doesn’t use a standard method to parse the query string, making this function quite suspicious. Let’s investigate further.
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, " "));
}
Above function is simplifiled below the regex here is searching for first ?text=data
or &text=data
.
// Simplified for name = text
function getParameterByName(){
var url = window.location.href;
var regex = /[?&]text(=([^&#]*)|&|#|$)/;
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
Here, we have an advantage: we’re working with the entire URL, while the XSS function only checks for angular brackets in the hash and search sections. Our goal is to craft a payload that avoids both the hash and search parts. Time to open our trusty devtools!
As you can see we can use password
, pathname
, port
, username
sections of URL to hide our payload. My intial thought is to hide payload in username and password section and i tried the following.
This even satisfies our regex.
issue here is when we use window.location.href
username and password won’t be returned. As you can see my dev tools title i have visited https://test:pass@example.com
but window.location.href
is just https://example.com/
.
The only viable option left is the pathname
. If we can craft a path that contains a valid regex payload while still pointing to the challenge route, we can achieve XSS. This opens the door to path traversal—replacing randomroute
with our regex payload. However, the browser will simply step back in the directory structure and load the intended resource.
if we urlencode one /
we can achive what we want.
The link https://challenge-0125.intigriti.io/randomroute/..%2fchallenge
loads the challenge page instead of returning a 404. This means we can replace randomroute
with c&text=<img src=x onerror=alert(document.domain)>
.
https://challenge-0125.intigriti.io/c&text=<img src=x onerror=alert(document.domain)>/..%2fchallenge
Conclusion
The vulnerability highlighted here is called “parser differential” it typically refers to differences or inconsistencies in how various components or systems interpret the same input. here the URL input is parsed by 2 functions differently such that a inconsistencies occured. this is a pretty fun challenge by 0xGodson and kudos to intigriti for hosting such a good challenge.