CVE-2026-42945 NGINX Rift — 18年无人发现的堆溢出RCE
渗透测试
CVE-2026-42945是NGINX rewrite_module中隐藏18年的堆溢出漏洞(CVSS 9.2)。根因是脚本引擎两阶段处理中is_args标志不一致,导致缓冲区分配不足。利用跨请求堆风水覆盖ngx_pool_t.cleanup指针,最终调用system()实现未认证RCE。文章包含完整复现踩坑记录与SRC实战拓展。
一、引子:18年,NGINX心脏里藏着一个堆溢出 ------------------------ 2026年4月,depthfirst的安全分析系统在自动审查NGINX源码时,发现了一个埋了18年的堆缓冲区溢出。 说"埋了"不太准确——这个漏洞从NGINX 0.6.27开始就存在了。2008年被引入,2026年才被发现,经历了18个年头。这段时间里,NGINX从一个小众的异步Web服务器变成了全世界部署量最大的Web服务器之一,跑在Netflix、Cloudflare、OpenResty、以及无数Kubernetes入口控制器的底层。而这个漏洞,一直睡在rewrite模块的代码里。 depthfirst的系统在"一次性接入NGINX源码后自主发现了它",随后顺手还发现了另外三个内存破坏漏洞——CVE-2026-42946、CVE-2026-40701、CVE-2026-42934。但最严重的是这个:CVE-2026-42945,CVSS 9.2,堆缓冲区溢出可导致未认证远程代码执行。 我知道很多人看到"NGINX漏洞"的第一个反应是——NGINX不是号称内存安全吗?不是用C写的吗?不是跑在最前面挡子弹的吗? 说实话,NGINX的代码质量在同类项目中确实是顶级的。但顶级不代表没有漏洞,尤其当你在同一个设计上迭代了18年,一些早期的设计假设可能会在后来的功能扩展中被突破。这次的问题就出在脚本引擎的两阶段设计上——一个标志位。 **18年,一个标志位的值错误,一次堆溢出,RCE。** 我先把攻击的完整流程画出来,方便读后面的章节时有个全局视角。  上图展示的是整个攻击链条。接下来我逐层拆解。 二、根因:is\_args标志的两阶段陷进 --------------------- 这个漏洞埋得有多深呢?它不在某个第三方库,不在某个可选模块,而是在NGINX核心引擎的脚本执行机制里。 NGINX的脚本引擎在处理rewrite、set等指令时,使用一个两阶段设计: 第一阶段:遍历所有指令,计算最终字符串需要的缓冲区大小。 第二阶段:分配缓冲区,再次遍历所有指令,把数据复制进去。 这个设计本身没什么问题——先算后写是防止缓冲区溢出的标准做法。问题出在一个细节上。 当rewrite指令的替换字符串中包含问号(`?`)时,NGINX需要把后面的部分当作查询参数(query string)处理。这时候会调用`ngx_http_script_start_args_code`,把`e->is_args`设为1。这个标志会告诉后续的指令:"现在开始,字符串中的特殊字符需要进行URI转义"。 关键来了。两阶段处理中,第一阶段用的是**一个全新的、清零过的子引擎**(`ngx_memzero(&le, ...)`),第二阶段的复制用的是主引擎。`is_args`标志在子引擎上是0,在主引擎上是1。 看代码逻辑更清楚: ```php 第一阶段(长度计算,使用子引擎le): le = { .is_args = 0 } // 清零后的子引擎 → 进入else分支,返回原始长度 第二阶段(复制,使用主引擎e): e = { .is_args = 1 } // 因为rewrite含?被设置了 if ((e->is_args || e->quote) && ...) { // 调用ngx_escape_uri,每个可转义字符从1字节扩展为3字节 // 缓冲区大小 = raw_size(基于第一阶段计算的原始长度) // 实际写入 = raw_size + 2*N (每个可转义字符多写2字节) // → 堆溢出 } ``` 那些被扩展的字符包括什么?加号(`+`)被转义为`%2B`,空格被转义为`%20`,问号被转义为`%3F`。一个字符变成三个字符,两倍的额外写入。而缓冲区是按一个字符一个字符的大小分配的。 这里的`+`号是个关键。在URI中,`+`通常表示空格,但`ngx_escape_uri`在处理`NGX_ESCAPE_ARGS`模式时会把`+`也转义。于是攻击者可以用一串`+`号来精确控制溢出的大小。 **说白了,这个漏洞的本质是:两个阶段看到的is\_args值不一样,一个以为不需要转义,一个以为需要转义,结果缓冲区就少算了一大截。** 这个bug什么时候被引入的呢?2008年,NGINX 0.6.27。当时NGINX还是俄罗斯一个小众项目,没人会想到18年后它能覆盖全球三分之一的网站。  三、环境搭建:从源码编译到起服务 ---------------- 开始复现之前,我要先说明一点:这个漏洞的PoC环境依赖一些特定的设置,直接拉个nginx镜像跑是不行的。 ### 踩坑一:特定commit depthfirst的PoC用了一个特定的NGINX commit:`98fc3bb78`。这不是任何一个release tag,而是master分支上的一个历史提交。对应的NGINX版本大概在1.29.x到1.30.0之间。PoC的`setup.sh`编译的就是这个commit的源码。 如果你自己手动搭环境,需要确保用的是受影响的版本(0.6.27 ~ 1.30.0),而且配置中必须有`rewrite`含`?`和`set`的组合。 ### 踩坑二:nginx.conf的精确配置 复现这个漏洞对nginx.conf有精确要求。depthfirst提供的配置是这样的: ```nginx server { listen 19321; request_pool_size 7920; connection_pool_size 4096; client_header_buffer_size 2048; location ~ ^/api/(.*)$ { rewrite ^/api/(.*)$ /internal?migrated=true; set $original_endpoint $1; } location /internal { internal; proxy_pass http://backend; proxy_read_timeout 60s; } location /spray { client_body_in_single_buffer on; proxy_pass http://backend; proxy_read_timeout 60s; } } ``` 几个关键细节: 第一,`request_pool_size 7920`。这个值控制每个请求分配的内存池大小,直接影响堆布局。不同系统可能需要调整这个值。 第二,`client_body_in_single_buffer on`。这个指令强制把请求体放在单一块连续内存中,是堆喷的关键。少了这行,POST body被分片存储,堆布局就没法精确控制。 第三,`/spray`和`/api/`路径分别承担不同角色。`/spray`用于堆喷发送payload,`/api/`用于触发溢出。 ### 踩坑三:ASLR必须关闭 PoC用硬编码的堆基址(`0x555555659000`)和libc基址(`0x7ffff77ba000`)。如果ASLR开启,这两个地址每次启动都不一样,PoC直接失效。 PoC在20个候选偏移之间循环,范围从`0x05a427`到`0x10d467`。每个候选对应堆内存中一个可能的位置,覆盖了NGINX单worker进程在低并发下堆分配的主要区域。每个候选最多尝试10次,每次重新做堆喷重新布局。如果目标系统的堆分布和预期不同,200次尝试可能全部失败,需要人工调整偏移数组。这个循环机制是PoC能保持高成功率的关键——它不需要一次就命中,而是通过多次尝试来抵消堆布局的微小波动。 entrypoint.sh通过`setarch x86_64 -R`来禁用ASLR: ```bash exec setarch x86_64 -R /nginx-src/build/nginx -p /app -c /app/nginx.conf ``` 在生产环境上ASLR是默认开启的——所以这个PoC在真实目标上无法直接使用。后面我会专门讨论ASLR绕过的问题。 ### 踩坑四:端口和依赖 服务跑在19321端口(不是80/443)。后端server.py监听127.0.0.1:19323,作为`proxy_pass`的目标。这个后端必须在前启动,不然NGINX的`proxy_pass`会报错。 后端代码本身很简单,就是一个带延迟能力的HTTP服务器: ```python class BackendHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): delay = float(self.headers.get('X-Delay', '5')) time.sleep(delay) self.send_response(200) ... ``` `X-Delay`头的默认值是5秒。这个延迟在堆风水过程中至关重要——它让连接保持打开状态,为跨请求的内存布局操作提供时间窗口。 ### 关于PoC的地址硬编码 PoC中有两个硬编码地址,分别对应堆基址和libc基址: ```python HEAP_BASE = 0x555555659000 # 堆的起始地址 LIBC_BASE = 0x7ffff77ba000 # libc的基址 SYSTEM_OFFSET = 0x50d70 # system函数在libc中的偏移 ``` 这些地址在Docker环境(Ubuntu 22.04 + 特定libc版本 + ASLR关闭)下是确定的。但如果你换了操作系统版本或者libc版本,`system`的偏移就变了。我一开始在Ubuntu 24.04上跑,system\_offset就不是0x50d70了,PoC死活不成功。 depthfirst提供的PoC在`setup.sh`和Dockerfile中固定了环境,就是为了避免这类问题——Ubuntu 22.04、固定libc版本、固定NGINX commit。如果你的环境跟Docker不一样,需要自己计算libc中system的偏移。 ### 快速复现步骤 ```bash git clone https://github.com/DepthFirstDisclosures/Nginx-Rift.git cd Nginx-Rift ./setup.sh docker compose -f env/docker-compose.yml up -d python3 poc.py --shell ```      不出意外的话,你会看到一个反弹shell。但这"不出意外"的前提是上面说的四个条件全部满足。我自己在搭建的时候在这个环节卡了三次——第一次是端口映射没配好,第二次是`X-Delay`超时时间不够,第三次是宿主机和容器内的libc版本不一致导致`system`地址偏移不对。每次卡住都得回头看日志、看配置、找原因。 **讲真的,复现CVE环境最花时间的往往不是漏洞本身,而是把环境调到刚好能触发的状态。** 四、PoC复现:从HTTP请求到system() ------------------------ depthfirst的PoC脚本浓缩了整个利用过程。我拆开来看每个步骤做了什么。 ### 第一步:堆喷(Spraying) PoC发送20个POST请求到`/spray`路径,每个请求体4000字节,包含精心构造的伪造数据结构。这些请求的body被`client_body_in_single_buffer`强制放在连续内存中。 每个POST body的结构是这样的: ```php [伪造的ngx_pool_cleanup_s] + [填充] + [更多伪造结构] + [命令字符串] ``` 伪造的cleanup结构中: - handler指向libc中的`system`函数(通过硬编码的libc基址 + 偏移`0x50d70`) - data指向紧随其后的命令字符串 spray请求之间间隔5毫秒,避免连接被合并导致堆布局不符合预期。 ### 第二步:触发溢出 PoC建立两个并行的连接: 连接A:发送一个精心构造的HTTP请求,URI路径`/api/`后跟: ```python payload = "A" * 349 + "+" * 969 + target_bytes.decode("latin-1") ``` 连接B:一个正常的HTTP请求,作为"受害者",它的请求池被布局在连接A的池旁边。 当NGINX处理连接A的请求时,rewrite指令被触发,`is_args`被设为1,`ngx_escape_uri`开始工作。每个`+`号被扩展为`%2B`,写入的字节数远超分配的缓冲区大小。溢出从连接A的请求池开始,一路覆盖到相邻的连接B的请求池。 ### 第三步:覆盖cleanup指针 溢出的目标偏移是64字节处的`cleanup`指针。攻击者的payload用计算好的地址覆盖了这个指针,指向第一步堆喷中布置的伪造`ngx_pool_cleanup_s`结构。 ### 第四步:触发system() 溢出发生后,攻击者关闭连接B。NGINX调用`ngx_destroy_pool`销毁连接B的请求池。这个函数遍历`pool->cleanup`链表,调用每个cleanup节点的handler: ```c void ngx_destroy_pool(ngx_pool_t *pool) { ngx_pool_cleanup_t *c = pool->cleanup; while (c) { if (c->handler) { c->handler(c->data); } c = c->next; } // ...(后面的释放逻辑,但因为cleanup已被篡改,在handler里就执行了system) } ``` `c->handler`已经被指向了`system`,`c->data`指向了命令字符串。于是`system("command")`被执行。 ### 为什么ngx\_destroy\_pool不提前崩溃 这个利用有一个反直觉的地方:cleanup指针之前的64字节(d、max、current、chain、large等字段)已经被URI安全字节覆盖了,这些字段被破坏后,任何涉及内存分配、释放、链表遍历的操作都会导致段错误。为什么destroy\_pool没有被这些损坏字段绊倒? 答案在NGINX的内存池实现中。`ngx_destroy_pool`的执行顺序是:先遍历cleanup链表并执行handler,然后遍历large链表释放大块内存,最后释放整个内存池。关键点在于:cleanup链表的遍历只依赖`pool->cleanup`指针和`c->next`指针,不涉及d、max、current、chain等字段。只要cleanup指针指向的地址是有效的(指向堆喷布置的伪造结构体),handler就能正常执行。 large链表的遍历和后续的释放操作确实会因为被损坏的字段而崩溃,但那是在handler执行之后了。system("command")已经执行完成,RC E已完成。后续的crash不影响攻击效果,只是多了一条worker崩溃的日志。 这是一个很精巧的设计:NGINX的内存池清理顺序恰好让攻击者可以在一次溢出中完成代码执行,而不需要在同一个payload中同时修复所有损坏的字段。如果cleanup链表遍历发生在其他字段被解引用之后,这个漏洞就只能DoS不能RCE。 **说白了,整个利用过程就是:先在地雷阵里埋一个触发装置,然后让系统自己踩上去。** ### PoC内部的安全地址过滤 PoC代码中有一个容易被忽略但非常关键的细节:地址安全验证。攻击者在覆盖cleanup指针之前,需要确保写入的地址不会在后续的内存操作中被截断或破坏。 NGINX内部使用`ngx_vslprintf`来格式化日志和错误消息,这个函数对某些字节值有特殊处理——具体来说,如果目标地址的某个字节落在危险范围内,`ngx_vslprintf`会提前截断字符串,导致地址写入不完整。 PoC中的地址过滤逻辑如下: ```python _t = [0xffffffff, 0xd800086d, 0x50000000, 0xb8000001, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff] ``` 这个位掩码数组定义了8个字节的过滤规则。每个掩码对应地址中一个字节位置的允许值范围。如果地址的某个字节落在掩码标记的危险范围内,该地址被丢弃,换下一个候选。 这个细节说明了一个问题:堆利用不是简单的"把地址写进去就行了",而要考虑目标程序内部对内存中数据的处理方式。同一个地址,在A程序里是一个有效的函数指针,在B程序里可能因为内部格式化函数的特殊处理而变成无效地址。  ### PoC脚本的执行逻辑 整理一下PoC的完整执行流程: ```php for each candidate_addr in PREREAD_HEAP_OFFSETS: for attempt in range(TRIES_PER_CANDIDATE): 1. 发送N_SPRAY个POST请求到/spray → 堆喷 2. 建立连接A(畸形请求) + 连接B(受害者) 3. 发送溢出payload 4. 关闭连接B → 触发ngx_destroy_pool 5. 发送探测请求检查目标是否存活 6. 若崩溃 → 判定利用成功,system(cmd)已执行 ``` 五、堆风水:跨请求的精确内存布局 ---------------- 这一章深入技术细节。如果你对堆管理的底层机制不感兴趣,可以跳过直接看SRC拓展部分,但如果你想知道这个漏洞为什么能用了18年才被发现,这一章是答案。 ### ngx\_pool\_t 结构 NGINX使用自己的内存池管理机制(`ngx_pool_t`)。每个HTTP请求分配一个独立的内存池,请求处理完后一次性销毁。这种设计减少了频繁malloc/free的开销,但也意味着:如果内存池的控制结构被破坏,整个请求处理流程都会受影响。 `ngx_pool_t`在64位系统上的布局: ```php 偏移 大小 字段 说明 0 16 d 内存池数据区(current指针、链表等) 16 8 max 最大分配大小 24 8 current 当前分配位置 32 8 chain 缓冲区链表 40 8 large 大块内存链表 48 8 — 对齐填充 56 8 — 对齐填充 64 8 cleanup ★ 清理函数链表(攻击目标) 72 8 log 日志 ``` 溢出是连续的。要覆盖偏移64处的`cleanup`,必须从偏移0开始一路覆盖前面的所有字段。如果前面的字段被破坏且被解引用,就会在到达目标之前触发崩溃。 ### 跨请求布局的关键 攻击者利用的是NGINX多连接的内存分配特性:连续到达的请求,其内存池在堆上是相邻分配的。 这里的难点在于精确控制两个池在堆上的相对位置。NGINX的内存池是从底层操作系统的堆(通过malloc)分配的,两个连续malloc调用返回的地址不一定相邻——中间可能有其他元数据或已分配块。PoC通过反复尝试来解决这个问题:每个候选偏移尝试10次,每次重新发送spray请求重新布局堆。在20个候选偏移 × 10次尝试 = 200次循环中,总会有一次布局精确命中。 每次尝试的时间窗口控制也很讲究。两个连接的建立时间差如果太大,池之间的间距就会受到其他分配操作的干扰;如果太小,连接可能被合并处理。PoC在spray请求之间用5毫秒间隔,在连接建立后用X-Delay头保持后端连接打开,确保池在销毁前一直存活。 还有一个细节:PoC选择先打开连接A的部分头部(不补全,让NGINX保持等待更多头部数据的状态),再打开连接B,最后补全A的头部。这确保了池A先分配、池B在其后分配,溢出方向从A到B是正向的。如果分配顺序相反(B先分配、A在其后分配),溢出数据就会写入无关的内存区域,攻击失败。 PoC的策略: - 先发送初始连接A的部分HTTP头部 → NGINX分配请求池A - 然后发连接B的请求 → NGINX在池A相邻位置分配池B - 补全连接A的头部 → 触发溢出,从池A写到池B - 池B的cleanup指针被覆盖 这个方法之所以能工作,是因为`ngx_destroy_pool`在处理cleanup链表时**不会解引用被破坏的分配器元数据**。它只遍历cleanup链表,调用handler,不碰其他字段。所以即使d、max、current等字段被URI安全字节损坏,只要清理函数被执行,RCE就完成了。 ### 为什么需要POST body堆喷 URI安全字符限制了攻击者的payload。URI中不能包含空字节,不能包含某些控制字符,而且payload经过`ngx_escape_uri`处理后长度会变。 攻击者的解法是:在触发漏洞的同一个连接中无法放完整的payload,那就先通过其他连接把payload放到堆上,然后在触发连接中只放一个指向payload的指针。 POST body是原始数据流,不受URI安全字符的限制。攻击者通过`/spray`端点发送包含空字节和任意二进制数据的POST请求,这些数据被`client_body_in_single_buffer`放在连续内存中,形成完整的伪造`ngx_pool_cleanup_s`结构。 多一步。但这一步绕过了URI安全字符的限制,是让这个漏洞从"能崩溃"变成"能RCE"的关键。 **一句话:URI控制溢出位置,POST body控制溢出内容,两者组合才构成完整的利用原语。**  六、ASLR绕过的可能性与现实 --------------- 这一节要说实话,因为网上很多CVE分析文章对ASLR绕过的问题要么不提,要么一笔带过。我觉得这事得说清楚。 ### PoC的假设 depthfirst的PoC假设ASLR是关闭的。在Docker环境中通过`setarch -R`实现。硬编码了堆基址和libc基址,不需要任何信息泄露。 在真实生产环境中,ASLR基本都是开启的。这意味着: - 堆基址每次启动随机 - libc基址每次启动随机 - golang binary、PIE编译的NGINX等,代码段基址也是随机的 PoC在ASLR开启的环境下直接跑,大概率结果是segfault而非RCE。 ### NGINX多进程架构对ASLR的影响 NGINX使用多进程架构:一个master进程fork出多个worker进程。在fork时,worker进程会继承master进程的完整内存布局。关键点是:**如果worker进程崩溃,master会fork一个新的worker,新worker的内存布局与崩溃前的worker完全一致。** 这意味着什么?意味着攻击者可以在一个worker上尝试部分覆盖,如果crash了,下一个worker的地址空间和crash前一样,可以接着试。 这些技巧包括: - 单字节覆盖(只改`cleanup`指针的最低字节,在不均匀的堆布局中偏移到另一个有效地址) - 双字节覆盖(覆盖低2字节,在64位地址空间中最多产生65536种可能,分多次尝试) - 部分覆盖+进程fork重试 但这些方法在实际利用中都非常受限。单字节覆盖需要堆上的目标对象正好在同一个256字节范围内;双字节覆盖需要65536次尝试(每次尝试都要crash一个worker然后等master重新fork);而且NGINX的重启时间不是即时的,多次crash会触发监控告警。 ### 组合利用视角 在真实攻击场景中,CVE-2026-42945不太可能作为一个独立的RCE漏洞被利用。更现实的场景是:攻击者先通过其他漏洞(信息泄露、路径遍历、SSRF等)获取了地址信息,然后用这个漏洞完成代码执行。 ASLR不是不可绕过的,但它需要额外的原语。这个漏洞的严重性不因为ASLR而降低——因为它提供了从内存破坏到代码执行的完整路径。缺少的是地址泄露这一步,但那是另一个漏洞的问题。 **说白了,ASLR是拦路虎,但不是断头路。它让利用难度从"一个HTTP请求"变成了"一个HTTP请求+信息泄露",而不是"不可能"。** ### 在ASLR开启场景下的可利用性评估 如果做一个量化的评估:CVE-2026-42945在ASLR关闭环境下(如部分Docker默认配置、嵌入式系统、遗留系统)的可利用性评级为"高"——只需要一个HTTP请求,无需其他前置条件。在ASLR开启的标准服务器环境下,评级降为"低—中"——需要额外的信息泄露漏洞配合。 但这里要区分两个概念:漏洞的严重性(CVSS 9.2)和利用的现实难度。CVSS评估的是漏洞本身的属性(攻击向量、攻击复杂度、权限要求等),不是利用的容易程度。9.2分说明这个漏洞的危害潜力极大——一旦被利用,影响力是灾难级的。而利用的难度取决于目标环境的安全配置,这不是CVSS能反映的。 对我个人来说,这种需要信息泄露配合的漏洞比一键RCE更有学习价值——它迫使你去理解整个攻击面的布局,而不是背一个PoC命令。 七、实战拓展:如何在SRC/众测中寻找此类漏洞 ----------------------- 这一章讲怎么在真实场景中找到类似的漏洞。先说清楚边界:以下内容仅限你自建环境和获得授权测试的目标。未经授权的测试是违法的。 ### 指纹识别:找到NGINX 在SRC/众测中,第一步永远是确认目标用的什么Web服务器。NGINX的指纹包括: - HTTP响应头:`Server: nginx/1.24.0`(但不是所有目标都会暴露完整版本号) - 默认404页面:NGINX的404页是"404 Not Found",纯文本,没有apache的"Apache/2.4.41 (Ubuntu)"那种详细信息 - HTTP/2支持:NGINX 1.9.5+支持h2,如果目标支持HTTP/2且Server头是nginx,大概率是较新版本 - `X-Powered-By`头:NGINX不会默认加这个头,如果看到了说明中间有应用层代理 ### 配置探测:是否存在rewrite+set组合 这个漏洞需要`rewrite`含`?`和`set`同时存在。怎么在不触发漏洞的前提下探测配置? 一种思路是观察重定向行为。发一个请求到`/api/test-path`,观察响应: - 如果返回301/302且Location中包含`?migrated=true`,说明rewrite规则生效 - 如果location中包含原始路径参数的变化(如`/internal?`),说明替换字符串中含`?` 但探测`set`指令更难——它不改变HTTP响应,只在NGINX内部设置变量。以下间接信号可能有帮助: - 响应头中包含自定义变量值(如`X-Endpoint: test-path`) - 响应内容中插入了捕获的变量值 - 不同的捕获值导致不同的后端代理行为 实际上,这些信号在无损探测中的可靠性不是100%。配置探测是SRC场景中最难的部分。 ### 版本检测与影响评估 如果确认目标使用NGINX且版本高于1.30.0(或1.30.1+),则不受此漏洞影响。如果版本低于0.6.27,也不受影响(但谁还在用2008年以前的NGINX?)。 对于版本在0.6.27到1.30.0之间的目标,还需要确认配置中是否同时使用了`rewrite`含`?`和`set`。没有这个配置组合,漏洞无法触发。 ### 合法测试的边界 在SRC场景中,对CVE-2026-42945的测试应该注意: - 漏洞触发一定会导致worker crash → 单次失败即引起服务重启 - 对于多worker配置,单个worker崩溃不会导致整个服务不可用,但会中断该worker上的所有活跃连接 - 在生产环境上触发crash本身就可能违反SRC规则 - 建议的测试方法:提交版本指纹+配置分析报告,不进行实际的漏洞触发验证 - 如果SRC规则允许PoC,应使用非生产环境或明确标注的测试子域名 ### 使用场景推断:从业务逻辑反推配置 在渗透测试中,很多时候你无法直接读取目标的nginx.conf,但可以通过业务行为反推底层配置。以下是我总结的一些判断依据: 第一,URL路径中携带标识符且被后端消费的场景。比如`/api/v2/users/123`这样的RESTful路径,如果后端实际接收到的路径中包含了`?source=nginx`之类的查询参数,说明前端有一个rewrite规则在重写路径。如果再配合Set-Cookie头中出现了后端策略变量,就更有说服力了。 第二,多版本API共存场景。如果一个API同时支持`/v1/`和`/v2/`两个版本,且两个版本指向不同的后端服务,NGINX配置中通常会写: ```php location ~ ^/(v[12])/(.*)$ { set $api_version $1; set $api_path $2; rewrite ^ /internal/$api_version; } ``` 这种配置正好踩中了漏洞的触发条件——rewrite含`?`(可能隐藏在不同配置文件的更深处)配合set指令。 第三,国际化站点。`/zh-CN/products`和`/en-US/products`走不同流程的站点,NGINX层通常会set语言变量再做rewrite。这些站点往往是NGINX的前置代理配置而不是应用层框架配置。 这些推断不能作为确凿证据,但可以帮助你在测试中缩小范围,找到最可能受影响的路径。 ### 关于合法测试的重要提醒 有些平台的SRC规则不允许任何形式的拒绝服务测试。CVE-2026-42945的漏洞触发必然导致worker进程崩溃——这不是一个可以"温和触发"的漏洞。触发即crash。所以: - 在测试前务必确认目标平台的SRC规则是否允许PoC级测试 - 建议仅限于自建环境或获得明确书面授权的范围 - 如果SRC只允许信息收集级别的测试,提交版本指纹+配置分析即可 - 部分SRC接受"漏洞证明但不执行利用"的模式,即通过分析确认目标配置存在风险但不实际触发漏洞 ### 哪些场景更容易出现rewrite+set组合 基于我对NGINX配置的观察,以下场景更常使用`rewrite`+`set`组合: - API网关/反向代理:统一的前端入口,根据URL路径重写到不同的后端服务。这是最常见的场景 - 多语言站点:根据URL前缀(`/en/`、`/zh/`、`/ja/`)设置语言变量,然后rewrite到对应的页面处理逻辑 - A/B测试:通过rewrite捕获URL中的变体标识,set到upstream的header中转发给后端 - 遗留系统迁移:旧URL路径被rewrite到新系统的路径,同时set变量传递原始路径信息 这些场景中,rewrite含`?`可能是因为需要在重写后的URL中添加查询参数,或者多个rewrite规则串联时需要区分处理阶段。 **不过我承认,在SRC里找到并利用这个漏洞的难度不低。不是因为漏洞本身复杂,而是因为触发条件(确切的版本+rewrite含?+set+精确的堆布局+ASLR绕过)在真实环境中同时满足的概率不高。但作为漏洞分析学习,它的技术含金量非常高,记得测试前先报备!。** 八、修复验证与缓解措施 ----------- ### 修复版本 depthfirst团队在4月18日提交报告,NGINX团队在4月24日确认了其中4个漏洞,5月13日发布安全公告并推出修复版本。从发现到修复用了不到一个月。 - NGINX Open Source:升级到1.31.0或1.30.1以上 - NGINX Plus:升级到R36 P4、R35 P2或R32 P6以上 验证方法就是检查版本号: ```bash nginx -v # 或 /path/to/nginx -v ``` ### 缓解措施(如果不能立即升级) 生产环境中的版本升级往往不是一键完成的,涉及兼容性测试、灰度发布、回滚方案等一堆流程。如果短期内无法升级,以下措施可以降低风险: 1. **检查配置**:全局搜索配置文件中同时出现`rewrite`(含`?`)和`set`的location块 2. **移除不必要的rewrite**:很多rewrite规则可以通过修改应用逻辑避免 3. **避免在rewrite替换字符串中使用`?`**:如果必须包含查询参数,考虑用其他方式传递 4. **限制rewrite规则的输入**:对用户可控的URI路径进行严格校验,限制`+`等特殊字符 5. **Web应用防火墙(WAF)规则**:添加对包含大量`+`或`%2B`的URI请求的检测和拦截 需要注意的是,缓解措施只是降低风险,不能代替修复。这个漏洞的触发条件是配置层面的,而不是默认配置就存在的——这意味着只有部分NGINX部署受影响。但受影响的这些部署,往往在架构中承担着关键入口的角色。 九、尾声 ---- 写这篇文章的时候,我在想一个问题:一个18年没被发现的漏洞,到底是因为它藏得太深,还是因为没人真正去翻NGINX核心引擎的代码? depthfirst的系统在"一次性接入NGINX源码后"就发现了它。没有fuzzing,没有人工代码审计,就是一个自动化分析系统跑了一遍源码。这让我觉得,2026年的漏洞发现能力已经进入了新的阶段——不再是"研究团队找漏洞",而是"分析系统自动找漏洞,研究人员做验证和利用"。 depthfirst发现了这个漏洞,NGINX团队用了不到一个月就修复了。从4月18日发现到5月13日发布安全公告,整个过程不到一个月。这是安全的理想流程:发现→确认→修复→披露。 但对于所有运行着NGINX 1.30.0及以下版本且使用了rewrite+set配置的服务器来说,这个漏洞从2008年就在这里。它见证了NGINX从一个俄罗斯程序员的副业项目变成全球基础设施。它见证了互联网流量的爆发式增长。 我写这篇文章不只是为了复现一个CVE,而是想传递一个观点:在代码中留下一个标志位的错误是很容易的,18年没人发现也是可能的,但最终总会有人——或者某个系统——找到它。安全不是一个终点,而是一个持续的过程。
发表于 2026-08-04 17:48:05
阅读 ( 52 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
zee
4 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!