

前两天正准备下班,安全群里突然炸了——CVE-2026-42945,代号 NGINX Rift,CVSS 9.2。我一开始还想着又是哪个小众模块的问题吧,结果点进去一看,好家伙,0.6.27 到 1.30.0 全版本覆盖,2008年引入的代码缺陷,18年没人发现。全球1.3亿个网站直接暴露,中国就有2500多万个实例。
最离谱的是,攻击者不需要任何认证,发一个HTTP请求就能打崩你的worker进程,特定条件下还能RCE直接拿shell。我当时脑子里的第一反应就是——我管的那几十台NGINX,rewrite规则里到处都是、2,这不得全中招?
赶紧泡了杯咖啡,开始了漫长的排查之夜。今天把整个过程写下来,希望还在犹豫要不要升级的朋友能有个参考。
文档地址:
https://access.redhat.com/security/cve/cve-2026-42945
https://nvd.nist.gov/vuln/detail/CVE-2026-42945
先说结论:问题出在 ngx_http_rewrite_module,具体是 src/http/ngx_http_script.c 里的 ngx_http_script_regex_replace 函数。本质是一个堆缓冲区溢出,但触发方式特别刁钻。
NGINX处理rewrite规则的时候,内部有个标志位叫 is_args,用来标记URI里有没有查询参数(就是问号?后面的部分)。遇到问号,这个标志位就设成1。
问题在哪呢?当rewrite的替换串里包含?符号时,NGINX的脚本引擎会做两遍遍历——第一遍算长度,第二遍拷贝数据。但这两遍用的不是同一个引擎上下文。
第一遍,长度计算阶段,NGINX实例化了一个全新的"子引擎",状态全部清零。这时候is_args默认是0,所以它按原始字节数来算捕获组(、2)的长度,完全没考虑后续的转义膨胀。然后按这个偏小的数字去分配堆内存。
第二遍,实际拷贝阶段,切回了保留完整上下文的"主引擎"。主引擎里is_args是1,因为替换串里有?。于是NGINX对捕获组的内容执行了参数转义——ngx_escape_uri以NGX_ESCAPE_ARGS模式运行。这一转义不要紧,%变成%25,一个字节膨胀成三个字节。原本1000字节的输入,转义完变成3000字节,但缓冲区只分配了1000字节。
多出来的2000字节直接溢出,覆盖相邻内存。
而且溢出的内容还不是随机乱码,是攻击者输入经过转义后的结果——可控的内存破坏。这才是最可怕的。
这点必须说清楚,不然容易引起恐慌。触发漏洞需要同时满足三个条件:
条件一:rewrite的正则里用了未命名捕获组,就是1、2这种。如果你用的是命名捕获(?<name>...),不受影响。
条件二:替换字符串里包含问号?。因为?是URI路径和查询参数的分界符,NGINX看到它会改变后续的转义处理逻辑。
条件三:同一个配置块里,这条rewrite后面紧跟着另一条rewrite、if或set指令。 因为需要第二条指令触发脚本引擎的重新评估,才能让那个错误的is_args状态继续发挥作用。
三个条件缺一不可。但问题是——这三个条件组合在一起,在生产环境里其实挺常见的。
看看这个PHP前端控制器的配置,WordPress、Laravel、ThinkPHP几乎都是这种写法:
location / {
rewrite ^/(.*)$ /index.php?$1 break;
rewrite ^/admin/(.*)$ /admin/index.php?$1 break;
}再看API网关的路由转发:
location /api/v1/ {
set $service "user-service";
rewrite ^/api/v1/(.*)$ /$1?$args break;
if ($request_method = POST) {
proxy_pass http://$service:8080;
}
}我扫了一遍自己管的机器,光是把路径改写成query string的rewrite就有二十多处。有些是五年前做URL迁移留下的,有些是老项目的WordPress伪静态配置,一直没动过。你说这些配置危险吗?单独看每一条都挺正常,但叠在一起就踩中了漏洞触发条件。
我也纳闷,NGINX这种全球几亿人在用的东西,代码审查不知道做了多少轮,怎么就这么个bug藏了18年?
想了想其实能理解。这不是那种经典的strcpy缓冲区溢出,它是逻辑型漏洞——标志位管理错误。编译器不会报警,因为代码语法完全合法。模糊测试也很难触发,因为需要特定的配置组合。运行时大多数情况下只是内存被轻微覆写,不会立即崩溃,可能就是某个请求偶尔返回个奇怪的结果,谁会往堆溢出上想?
而且触发条件确实苛刻,单个location里连续两条rewrite且第一条包含?和捕获组,这种配置不算少见但也不算特别普遍。就是这种"常见但不普遍"的特性,让它在18年间完美逃过了所有检测。
直到depthfirst的AI安全分析系统点了一下鼠标,6小时扫描就揪出来了。说真的,看到这个我后背有点发凉——AI找漏洞的效率太恐怖了。
说回实操。收到漏洞公告后我做的第一件事不是急着升级,而是先确认自己的环境到底有没有踩中触发条件。
第一步,查版本:
nginx -v线上大部分机器跑的是1.24.x和1.26.x,全部在受影响范围内。
第二步,导出完整配置并扫描危险rewrite:
sudo nginx -T > /tmp/nginx-all.conf 2>/tmp/nginx-test.log
rg -n 'rewrite\s+.*\$\d+.*\?' /tmp/nginx-all.conf这条命令找的是rewrite里同时出现未命名捕获引用(1、2)和问号的地方。不是完美的检测器,但第一轮排查够用了。
扫完结果出来,我心里咯噔一下——命中了17处。然后逐一打开配置看,确认这些rewrite后面有没有紧跟rewrite、if或set指令。最终确认有6处完全命中触发条件,其中3处是对外暴露的API网关。
第三步,查有没有中招痕迹:
grep -i "exited on signal 11\|SIGSEGV\|core dumped" /var/log/nginx/error.log还好,没有发现worker异常退出的记录。但这也不能说明没被利用过——攻击者完全可以小流量慢慢试探,不触发崩溃的那种。
确认配置命中后,我在隔离环境里做了复现验证。这里强调一下,绝对不要在生产环境跑PoC。
用Docker起一个1.30.0的NGINX:
mkdir -p /tmp/nginx-rift-lab/conf.d写一个实验配置到 /tmp/nginx-rift-lab/conf.d/default.conf:
server {
listen 8080;
server_name _;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log notice;
location / {
rewrite ^/u/([0-9]+)/(.+)$ /profile.php?id=$1&tab=$2 last;
set $rewrite_marker 1;
return 200 "lab\n";
}
}启动容器:
docker run --rm --name nginx-rift-lab \
-p 8080:8080 \
-v /tmp/nginx-rift-lab/conf.d:/etc/nginx/conf.d:ro \
nginx:1.30.0另一个终端盯着日志:
docker logs -f nginx-rift-lab然后用公开的PoC(GitHub上 cipherspy/CVE-2026-42945-POC)发送触发请求。结果很直接——worker进程崩了,error log里出现了exited on signal 11。换成1.30.1的镜像再跑同样的测试,不再崩溃。
验证完毕。确认受影响,必须升级。
升级总需要时间,尤其生产环境还得走变更审批。在升级之前,可以先改配置来瓦解触发条件链。
最推荐的做法是把未命名捕获改成命名捕获:
# 危险写法
rewrite ^/users/([0-9]+)/profile/(.*)$ /profile.php?id=$1&tab=$2 last;
set $rewrite_marker 1;
# 安全写法
rewrite ^/users/(?<user_id>[0-9]+)/profile/(?<section>.*)$ /profile.php?id=$user_id&tab=$section last;
set $rewrite_marker 1;看起来只是把1、2换成了user_id、section,但这个语法层面的细微变化改变了脚本引擎在参数解析时的作用域边界和状态传递链条,直接绕开了底层缺陷。
改完测试配置:
sudo nginx -t
sudo systemctl reload nginx但这只是临时处理,二进制还在受影响版本里,能升级还是尽快升级。
我线上主要是Debian系统,这里以Debian为例写一下手动升级流程。其他发行版逻辑差不多。
升级前先备份:
sudo cp -a /etc/nginx /etc/nginx.bak.$(date +%F-%H%M)
sudo nginx -t安装基础依赖:
sudo apt update
sudo apt install curl gnupg ca-certificates lsb-release debian-archive-keyring -y下载NGINX官方签名key:
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
| sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null顺手验证一下fingerprint,官方文档给的是573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62:
gpg --dry-run --quiet --no-keyring --import --import-options import-show \
/usr/share/keyrings/nginx-archive-keyring.gpg添加NGINX官方mainline源:
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
https://nginx.org/packages/mainline/debian $(lsb_release -cs) nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list让apt优先用nginx.org的包:
echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" \
| sudo tee /etc/apt/preferences.d/99nginx更新并安装:
sudo apt update
apt-cache policy nginx
sudo apt install nginx如果之前装的是Debian自带的拆分包(nginx-common、nginx-core),安装过程会提示配置文件保留还是覆盖。我的习惯是先保留现有配置,升级后再手动diff。
检查版本和配置:
nginx -v
sudo nginx -t
sudo systemctl restart nginx
sudo systemctl status nginx --no-pager这里有个坑要注意——Debian仓库里的nginx版本可能还是1.26.3,看起来不像修复版本。Debian的稳定仓库经常是旧上游版本加安全backport,不一定把版本号直接抬到最新。nginx官方这次明确的修复边界是Open Source 1.30.1+或1.31.0+。所以判断时别只盯着nginx -v的主版本号,要么看Debian Security Tracker里对应包的状态,要么直接切到nginx官方源。
我选的是切官方源,升级到mainline 1.31.x,省心。
另外,升级后一定要restart而不是reload。reload只是重新加载配置,不会替换内存中正在运行的二进制程序。安全补丁升级后老worker可能还跑着旧代码:
ps -eo pid,ppid,cmd | grep 'nginx: (master|worker)'
sudo systemctl restart nginx这个必须单独说。Kubernetes官方维护的ingress-nginx控制器项目已经归档停止推进了,它最终正式版镜像里静态编译的是NGINX 1.27.1——受影响版本。你在宿主机上升级NGINX的RPM/DEB包,对容器里的控制器进程毫无帮助。
排查方法:
kubectl exec -n ingress-nginx <controller-pod> -- /nginx-ingress-controller --version如果确认是受影响版本,短期可以换用社区维护的、合并了上游安全补丁的分支镜像。长期建议加速向Kubernetes Gateway API迁移,别再依赖那个已经归档的ingress-nginx了。
升级完不是结束,还有两件事得做。
第一,重新扫一遍危险rewrite。升级能修漏洞,但那些历史rewrite不一定值得继续留着。能改成命名捕获就改掉,能删就删:
sudo nginx -T > /tmp/nginx-all-after.conf 2>/tmp/nginx-test-after.log
rg -n 'rewrite\s+.*\$\d+.*\?' /tmp/nginx-all-after.conf第二,检查ASLR状态。RCE利用有个前提条件是目标系统关闭了ASLR。确认所有对公网暴露的NGINX服务器都开启了ASLR:
sysctl kernel.randomize_va_space
# 应该返回2如果返回0或1,赶紧改成2,这是P0级别的安全基线问题。只有关闭ASLR的环境下,这个漏洞才可能被实打实地转化为RCE。
还有监控方面,在SIEM或日志平台上加个规则——一旦NGINX错误日志里大量出现worker process exited on signal 11,同时访问日志里有来自特定IP、包含超长编码字符的异常请求,立即告警。
这次CVE-2026-42945给我的触动挺大的。NGINX这种跑遍全球的基础设施,代码被无数人审查过,结果一个标志位管理错误就藏了18年。危险的不是什么高级功能,而是那些看起来再正常不过的rewrite配置——你可能只是五年前为了兼容旧URL写了几行1、2,然后这个入口一直挂在公网上。
AI找漏洞的时代已经来了。depthfirst的系统点一下鼠标,6小时就揪出了4个被官方确认的远程内存损坏漏洞。这对防守方是好事,但攻击方同样可以用AI来找零日。所以别再觉得"系统运行正常就不用更新"了,漏洞就在那里,只是还没被你发现而已。
简单总结一下行动清单:
nginx -v查版本,低于1.30.1就准备升级安全这事没有终点,只有持续的过程。希望这篇文章能帮到正在熬夜处理这个漏洞的你。