简介:本文深入剖析一个潜伏十五年的NGINX脚本引擎漏洞:因PCRE捕获状态在LEN与VALUE两轮评估间缺乏保存与恢复机制,导致缓冲区大小判断不一致,进而衍生出堆溢出与信息泄露两种攻击原语,并可组合实现无需认证的远程代码执行。文章系统梳理漏洞机理、影响面、受影响指令清单与修复版本,并给出配置审计与升级加固的工程实践建议。
在Web服务器与反向代理领域,NGINX因其事件驱动架构与低资源占用而被广泛部署。其配置语言中内置了一套轻量级“脚本引擎”,允许在map、rewrite、if、return等指令中嵌入正则表达式并引用捕获组(如$1、$2)。正是这套看似简单的表达式求值机制,隐藏着一个自2011年map指令支持正则表达式以来便长期存在的缺陷。
该缺陷的核心可以一句话概括:脚本引擎在对表达式进行两轮求值的过程中,缺少对PCRE捕获状态的保存与恢复。攻击者无需任何认证,仅通过精心构造的请求,即可诱使引擎在“测量缓冲区长度”与“写入实际数据”两个阶段得出不一致的结论,从而触发堆缓冲区溢出或信息泄露。前者可用于破坏相邻堆内存,后者可在单次请求中泄露足以绕过地址空间随机化的指针数据;两者组合,便可构建出高可靠性的远程代码执行链路。
这类漏洞的危险性不仅在于后果严重,更在于其静默性:由于触发路径依赖特定配置组合,管理员在常规运行中几乎不会察觉异常,漏洞因此得以长期潜伏。
NGINX的脚本引擎在需要生成动态字符串时,采用经典的“两轮评估”策略:
这一设计的初衷是避免反复扩容,提升性能。两轮求值共享同一个运行时请求上下文,其中包含一个名为r->captures的可变数组,用于暂存最近一次正则匹配的捕获组内容。
问题出现在如下场景:一个基于正则的map变量在求值过程中,其内部又执行了带正则的指令(例如在同一个请求上下文中,先由location或rewrite产生捕获组,再由map引用$1)。当map表达式在两次引用捕获组之间执行了另一次正则匹配时,新的匹配结果会静默覆盖共享的r->captures数组。
于是:
len_A;len_B。若len_B > len_A,VALUE轮写入的数据超出LEN轮分配的缓冲区大小,形成堆缓冲区溢出;若len_B < len_A,缓冲区被分配过大,实际写入后剩余部分未被初始化,随后在响应中回传,形成未初始化内存泄露。
堆缓冲区溢出原语:溢出内容完全由攻击者可控的捕获组数据决定,可精确覆盖相邻堆块中的结构体字段、指针或长度标记,为后续控制流劫持创造条件。
信息泄露原语:泄露的未初始化堆字节中往往包含堆指针与C库函数指针。攻击者仅需一个未认证的GET请求,即可获取这些指针,从而计算模块基址,绕过ASLR。
将两者结合,攻击者可以先用泄露请求获得地址布局,再通过若干连接进行堆喷射以提升布局可控性,最后用一次触发溢出的请求完成利用。公开分析指出,在启用完整ASLR的现代Linux发行版上,该链路可达到极高的稳定性。
该漏洞并非局限于某一指令的实现疏漏,而是涉及脚本引擎中多处调用点。据分析,缺陷分布于多个源文件的至少十余个独立调用位置,且同时影响HTTP与stream两大模块。
任何满足以下条件的配置都可能被利用:
location正则匹配、server_name通配、rewrite规则或if块中的正则条件;map变量,且其表达式引用了捕获组;location块内的不同指令之间也可能发生。这意味着漏洞的触发不要求配置“明显错误”,许多常见的动态路由、参数改写、日志格式化配置都可能无意中满足条件。
以下指令在求值过程中可能参与捕获组引用或触发中间匹配,属于需要重点审计的范围:
proxy_set_header、proxy_method、proxy_pass、fastcgi_param、uwsgi_param、scgi_param、grpc_set_headerreturn、add_headerrewrite、set、root、aliasaccess_log(含自定义日志格式中的变量展开)值得特别注意的是,还存在一种可独立利用的变体:使用命名捕获组(?P<name>...)的表达式。命名捕获组的缓存路径与编号捕获组不同,因此仅修复编号捕获组状态的补丁并不能覆盖该变体。审计时若只关注$1、$2而忽略$name形式,将留下缺口。
该漏洞已由上游维护方协调修复,修复的核心思路是在可能发生嵌套求值的调用点对捕获状态进行保存与恢复,确保LEN轮与VALUE轮看到一致的捕获内容。修复版本覆盖稳定版与主线版两条分支,并对商业订阅版本同步发布了补丁。
对使用者的实践建议:
map与其它正则指令产生求值交叠;对可疑配置进行隔离测试。可采用“静态检索 + 动态验证”的组合方式:
map块,标记其表达式是否引用捕获组;server/location上下文中是否存在其他正则指令;示意性的高风险配置模式(仅用于说明结构,不构成可用利用代码):
# 高风险模式示意:正则 location + 正则 map 引用捕获组map $uri $backend {~^/api/(?P<ver>v[0-9]+)/(.*)$ $ver; # 引用了捕获组}location ~ ^/svc/(.*)$ {proxy_set_header X-Origin $1; # 另一处捕获引用proxy_pass http://$backend; # 与 map 求值交叠}
$uri等内置变量。map与其它正则指令拆分到不同层级,减少共享状态被覆盖的机会。这一案例揭示了嵌入式脚本引擎的一类共性风险:共享可变状态 + 多阶段求值 + 不可信输入三者叠加,极易产生“测量与使用不一致”(TOCTOU的变体)问题。对平台与框架开发者而言,启示在于:
对运维与安全团队而言,则应将“配置语言本身即为攻击面”纳入威胁模型:正则表达式不仅是匹配工具,也是状态机,其求值副作用同样需要审计。唯有把配置当作代码来管理——版本化、评审、测试与灰度——才能在类似的长潜伏期缺陷被公开时快速定位自身暴露面并完成收敛。