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

  1. Since there is no X-Frame-Option in both pages, we can open them with iframes.
  2. We block the JavaScript thread with a No-Operation loop.
  3. 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.
  4. 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.