myClock XSS Challenge Solution Write-Up
Last month, I created an XSS challenge on my sandbox domain and named it myClock. The page contains JavaScript code that checks if the clock has been paused for 3 seconds. There are multiple ways to solve the challenge, and I allowed user interaction to encourage creative solutions. Let’s get started with an explanation of the challenge.
Note: If you are here to see solutions only, jump to the bottom of the page.
Note 2: In this write-up, I will explain the intended solution only.
Introduction
Initially, the challenge is only a client-side JavaScript code:
<!DOCTYPE html>
<html>
<head>
<title>My Clock v 1.0</title>
<script src="https://cure53.de/purify.js"></script>
<script src="http://sandbox2.ahussam.me/url.php?code=var Url='//ahussam.me';"></script>
<style>
#myClock {
font-size: 4em;
}
</style>
</head>
<body>
<h2>Rules</h2>
<ul>
<li>Execute <i>alert(domain)</i> on this page.</li>
<li>Hint: your payload is going to be clicked at <b>XX:XX:01</b>.</li>
<li>Only the latest browsers are supported.</li>
<li>User interaction is allowed.</li>
</ul>
<center>
<div id="myClock"></div>
<div id="myComment"></div>
</center>
<h2>Solvers</h2>
<ul>
<li><a href="https://twitter.com/Abdulahhusam">YOU! Send a DM.</a></li>
</ul>
<script>
var dirty = new URL(location).searchParams.get('comment');
var cleanHTML = DOMPurify.sanitize(dirty, { FORBID_TAGS: ['a'] });
document.getElementById('myComment').innerHTML = cleanHTML;
var link = new URL(location).searchParams.get('link');
if (link === "1") {
var myURL = url || "https://ahussam.me";
var newWindow = window.open(null, null, "toolbar=no,location=no,dialog=yes,personalbar=no,status=no,dependent=yes,menubar=no,resizable=yes,scrollbars=no");
if (newWindow) {
newWindow.opener = null;
newWindow.location = myURL;
}
}
function updateTime() {
var time = new Date();
var hours = time.getHours();
var minutes = time.getMinutes();
var seconds = time.getSeconds();
var clock = document.getElementById('myClock');
if (clock.innerHTML.indexOf(':') > -1) {
var sec = clock.innerHTML.split(':')[2];
if (parseInt(sec) + 3 < seconds) {
if (window.opener) {
exit();
} else {
location = location.hash.substr(1);
}
}
}
clock.innerHTML = placeHolder(hours) + ":" + placeHolder(minutes) + ":" + placeHolder(seconds);
function placeHolder(input) {
if (input < 10) {
return '0' + input;
} else {
return input;
}
}
}
document.addEventListener("DOMContentLoaded", function() {
window.setInterval(updateTime, 100);
});
</script>
</body>
</html>
I used DOMPurify to sanitize the comment before injecting the content into the DOM, which is safe unless @SecurityMB breaks it. I only forbade the anchor tag:
FORBID_TAGS: ['a']
The code is a very basic clock that checks if the DOM (clock) has been paused or not. In case it is paused for more than 3 seconds, it will set the location
to the location.hash
(sink). So the main idea is to freeze the DOM for a while since the payload will be clicked at XX:XX:01.
Technical Details
To begin with, we must find a way to freeze the DOM. I was expecting some new tricks to be submitted, and I was right! At the page head, we can find a way to XSS sandbox2.ahussam.me.
However, we must XSS sandbox.ahussam.me
, so let’s put that in our notes:
- We can XSS sandbox2.ahussam.me.
If you try to XSS sandbox2.ahussam.me
, there is a simple WAF that could be bypassed very easily. It wasn’t part of the challenge at the planning phase, but I expect you to bypass it though!
Another interesting thing is that the url variable isn’t defined because JavaScript is case-sensitive, so Url
isn’t the same as url
. Add this to our notes:
url
isn’t defined.
Let’s put them all together:
- We must freeze the DOM for 3 seconds.
- We have an XSS on sandbox2.ahussam.me.
- We must define url in sandbox.ahussam.me. We can define it using DOM Clobbering.
Well, the challenge has been solved at this point! How? Say welcome to Site-Isolation.
Chrome started using Multi-process Architecture to prevent a crashing tab to hang the whole browser’s process and to create a secure context to each tab so they don’t share the same thread. This could take a lot of time explaining the Process-Thread-Task on the operating systems. I will share resources about them. Let me quote from Chromium:
To support a site-per-process policy in a multi-process web browser, we need to identify the smallest unit that cannot be split into multiple processes. This is not actually a single page, but rather a group of documents from the same website that have references to each other. Such documents have full script access to each other’s content, and they must run on a single thread, not concurrently. This group may span multiple frames or tabs, and they may come from multiple sub-domains of the same site. So, a SiteInstance of sandbox.ahussam.me and sandbox2.ahussam.me is running on a single thread! A JavaScript code on sandbox2.ahussam.me can block the JavaScript on sandbox.ahussam.me! If we create iframes to sandbox.ahussam.me and sandbox2.ahussam.me on a page, they will share the same thread. The intended solution is the following:
Host it on your server:
var t = Date.now();
while (Date.now() - t < 4000) {
nop();
}
function nop() {}
Steps
- Since there is no X-Frame-Option in both pages, we can open them with iframes.
- We block the JavaScript thread with a No-Operation loop.
- The DOM of sandbox.ahussam.me will be blocked, and when the 4 seconds pass, the DOM clock will be late by more than 3 seconds.
- The JavaScript location window will be set to location.hash. Use the challenge page to understand how to reproduce. BTW, there is a way to solve this with window.open that you can check out here.
Solvers
- RootEval
- RenwaX23
- MichaB Bentkowski
- Guilherme Keerok
- Alex
Conclusion
We had fun and learned new tricks to freeze the DOM and playing around with site-isolation.