vm沙箱逃逸初探
前言
前几天遇到一个考察vm沙箱逃逸的题目,由于这个点是第一次听说,所以就花时间了解了解什么是沙箱逃逸。此篇文章是对于自己初学vm沙箱逃逸的学习记录,若记录知识有误,欢迎师傅们指正。
什么是沙箱
就只针对于node.js而言,沙箱和docker容器其实是差不多的,都是将程序与程序之间,程序与主机之间互相分隔开,但是沙箱是为了隔离有害程序的,避免影响到主机环境。为什么node.js语言要引入沙箱,这就要说说js语言中的作用域(也叫上下文)。说一大堆概念不如贴一段代码来的实在:
const a = require("./a") |
若没有exports将需要的属性暴露出来,我们是访问不到另一个包内的属性的。包与包之间是互不相通的,也就是说每一个包都有自己的作用域。我们知道JavaScript的全局变量是window。其中所有的属性都是挂载到这个window下的,当然,node也有全局变量,是global。全局变量能在包间访问,换句话说,所有的包都挂载在全局变量下。node执行rce需要引入process对象进而导入child_process模块来执行命令。然而,process是挂载到global上的。为了防止恶意代码影响主机环境,所以就引入沙箱,开辟一个新的作用域来运行不信任的代码。相较于其他作用域,它阻止我们从内部直接访问global全局变量。此后的逃逸也是在这个点做文章。
vm模块的作用
引入vm模块就是为了创建一个沙箱运行环境。先看一段代码:
const util = require('util'); |
vm.createContext函数,创建一个沙箱对象,在全局变量global外又创建一个作用域。此时sandbox对象就是此作用域的全局变量。vm.runInContext函数,第一个参数是沙箱内要执行的代码,第二个是沙箱对象。还有一个函数,vm.runInNewContext,是creatContext和runInContext的结合版,传入要执行的代码和沙箱对象。根据代码输出,我们可以看出沙箱内是不能访问到global。
如何逃逸
上文也说明了node要执行命令的前提是访问到process对象。那么逃逸的主要思路就是怎么从外面的global全局变量中拿到process。vm模块是非常不严谨的,基于node原型链继承的特性,我们很容易就能拿到外部全局变量。看一段简单的逃逸代码:
; |
运行结果为
很明显是逃逸出去了。如何做到的?这里的this是指向传递到runInNewContext函数的一个对象,他是不属于沙箱内部环境的,访问当前对象的构造器的构造器,也就是Function的构造器,由于继承关系,它的作用域是全局变量,执行代码,获取外部global。拿到process对象就可以执行命令了。继续看代码:
; |
执行结果:
console.log会执行node代码,从而调用构造器函数返回process对象导致rce。vm模块的隔离作用可以说非常的差了。所以开发者在此基础上加以完善,推出了vm2模块。那么vm2模块能否逃逸。
vm2相较于vm多了很多限制。其中之一就是引入了es6新增的proxy特性。增加一些规则来限制constructor函数以及___proto__这些属性的访问。proxy可以认为是代理拦截,编写一种机制对外部访问进行过滤或者改写。直接看文档中的例子,文档链接:ES6 入门教程
var proxy = new Proxy({}, { |
vm2的版本一直都在更新迭代。github上许多历史版本的逃逸exp,附上链接:Issues · patriksimek/vm2 · GitHub,至于vm2的逃逸原理分析,直接看大牛的文章,写的非常nice,文章链接:vm2沙箱逃逸分析-安全客 - 安全资讯平台。接着做个题感受一下。
[HFCTF2020]JustEscape
题目代码:
|
题目中提示我们不是php,当然eval函数也不只有php有。测试eval是js语言的,可以输入Error().stack。它的作用是返回代码的部分信息。
了解到引入了vm2沙箱。GitHub上有师傅发的逃逸脚本可以直接打。附上链接:Breakout in v3.8.3 · Issue #225 · patriksimek/vm2 · GitHub。有两个exp都能打通。看一下第一个exp:
(' + function(){ |
网上找半天都没有详细解释代码,于是自己琢磨了一下。若分析有误,希望师傅们指正。先来看看Object.preventExtensions函数,让一个对象不能再添加一个新的属性。如果尝试添加新的属性,就会抛出typeError。
上面代码尝试添加a属性。导致异常就会抛出TypeError对象。这里进行了一个操作,污染了TypeError的原型。TypeError是外部的对象,这里将它的原型添加get_process属性,并且在此基础上调用构造器函数,此时它 的作用域就是global了。抛出的TypeError对象由e捕捉,访问get_process属性,从原型里拿,进而触发构造器函数,返回process对象执行命令。
则第二个exp有点像上文vm2原理分析的案例二。
(' + function(){ |
创建一个代理,我猜测getOwnPropertyDescriptor函数用于Proxy第二个参数传递会发生异常。vm2内部抛出异常并捕获,然后vm2沙箱自己封装成一个对象再次抛出,被捕获,此时这个对象的作用域就变成了global,执行抛出的代码可以拿到process对象了。
截止目前,我们并不能做出这道题。因为还有waf过滤了许多关键词,这里就要用到一个点,利用js的模板文字绕过,直接看个例子就能明白。
把过滤掉的关键字都换成这种模板文字,最终payload为:
(function (){ |
打入payload即可获得flag。
结语
vm2版本一直在更新,而逃逸脚本也穷出不穷,而在去年也爆出了新的vm沙箱逃逸的cve。以后有时间继续琢磨沙箱逃逸脚本。此外贴上vm沙箱逃逸的优秀文章: