初识vm沙箱逃逸
初学者的学习笔记,没啥含金量。
0x01 基础知识
JavaScript和Nodejs之间有什么区别?
简单说就是JavaScript用于浏览器前端,Nodejs用在后端(服务器)。
什么是沙箱(sandbox)?
用于隔离恶意代码的环境,恶意代码即使在沙箱中被执行也没多大危害。
在Nodejs中,我们可以通过引入vm模块来创建一个“沙箱”,但其实这个vm模块的隔离功能并不完善,还有很多缺陷,因此Node后续升级了vm,也就是现在的vm2沙箱,vm2引用了vm模块的功能,并在其基础上做了一些优化。
0x02 Node可以把字符串当代码执行
用一些参考文章中的栗子(在.js所在目录下创建一个age.txt):
1 | |
创建hello,js
1 | |
结果:
1 | |
age.txt中的var age = 18被当成字符串执行了。
不过这里有个问题:如果在当前作用域(也叫上下文)下已经有了个age,那么会报错。举个栗子:
1 | |
作用域是什么?举个栗子:
bye.js:
var age = 20
hello.js
1 | |
那么如何在一个文件中引入当前目录下,另一个文件中的元素?可以利用exports接口达到目的:
1 | |
hello.js
1 | |
若没有exports将需要的属性暴露出来,我们是访问不到另一个包内的属性的。包与包之间是互不相通的,即每一个包都有自己的作用域。

(这张图从别人那里偷的y1=bye,y2=hello)
global是什么?
在Node.js中,global是一个全局对象,类似于浏览器环境中的window对象。它可以在任何地方访问,无需引入或声明。
global对象包含了Node.js中的全局变量和全局函数。例如,console、setTimeout、setInterval等函数都是global对象的属性。可以通过global对象来访问这些全局函数和变量。
在Node.js中,global对象还可以用来创建全局变量。但是,为了避免全局变量的滥用,通常不推荐使用全局变量,而是使用模块的导出和导入机制来进行变量的共享和访问。
举个例子,我们在用console.log输出时并不需要写成global.console.log,其他常见全局变量还有process(一会逃逸要用到)。
看一个有关全局变量的栗子:
1 | |
1 | |
这次我并没有利用exports导入age,但hello.js确实访问了另一个包的属性。
此外,还有一种方法new Function:
在Node.js中,new Function是一个构造函数,用于创建一个新的函数对象。它接受一个或多个字符串参数,其中最后一个参数是函数体,前面的参数是函数的参数列表。
使用new Function构造函数可以动态地创建一个函数对象,而不需要提前定义函数的名称或函数体。这种方式可以在运行时根据需要创建函数,具有一定的灵活性,比如:
1 | |
0x03vm沙箱
因为沙箱的目的是要隔离恶意代码,那我们可以通过创建新作用域并让恶意代码在其中运行而不影响其它作用域,看几个常用的vm模块的API:
vm.runinThisContext(code):在当前global下创建一个作用域(sandbox),并将接收到的参数当作代码运行。sandbox中可以访问到global中的属性,但无法访问其他包中的属性。

1 | |
可以看到,在沙箱中把字符串当代码执行'localVar = "vm";',只影响沙箱这个作用域,对其它作用域没影响。
vm.createContext([sandbox]): 在使用前需要先创建一个沙箱对象,再将沙箱对象传给该方法(如果没有则会生成一个空的沙箱对象),v8为这个沙箱对象在当前global外再创建一个作用域,此时这个沙箱对象就是这个作用域的全局对象,沙箱内部无法访问global中的属性。
vm.runInContext(code, contextifiedSandbox[, options]):参数为要执行的代码和创建完作用域的沙箱对象,代码会在传入的沙箱对象的上下文中执行,并且参数的值与沙箱内的参数值相同。

1 | |
vm.runInNewContext(code[, sandbox][, options]): creatContext和runInContext的结合版,传入要执行的代码和沙箱对象。
vm.Script类 vm.Script类型的实例包含若干预编译的脚本,这些脚本能够在特定的沙箱(或者上下文)中被运行。
new vm.Script(code, options):创建一个新的vm.Script对象只编译代码但不会执行它。编译过的vm.Script此后可以被多次执行。值得注意的是,code是不绑定于任何全局对象的,相反,它仅仅绑定于每次执行它的对象。
code:要被解析的JavaScript代码
1 | |
script对象可以通过runInXXXContext运行。
0x03如何逃逸?
node执行RCE需要引入process对象进而导入child_process模块来执行命令。然而,process是挂载到global上的。前面的例子也说明了在沙箱中貌似不能访问到global,那么该如何逃逸?如何拿到process?
vm模块是非常不严谨的,基于node原型链继承的特性,我们很容易就能拿到外部全局变量。比如:
1 | |
结果:

这里的this是指向传递到runInNewContext函数的一个对象,他是不属于沙箱内部环境的,访问当前对象的构造器的构造器,也就是Function的构造器,由于继承关系,它的作用域是全局变量,执行代码,获取外部global。拿到process对象就可以执行命令了。
1 | |
一些其它情况:
1 | |
现在的this为null,并且也没有其他可以引用的对象,这时候想要逃逸要用到一个函数中的内置对象的属性arguments.callee.caller,它可以返回函数的调用者。
我们上面演示的沙箱逃逸其实就是找到一个沙箱外的对象,并调用其中的方法,这种情况下也是一样的,我们只要在沙箱内定义一个函数,然后在沙箱外调用这个函数,那么这个函数的arguments.callee.caller就会返回沙箱外的一个对象,我们在沙箱内就可以进行逃逸了。
举个栗子:
1 | |
解释下这个方法:先对toString方法进行了重写(重写不改变方法何时被调用)。console.log('Hello ' + res)时(一个对象与字符串发生了关系),就会调用toString方法。(感觉有点像PHP中的魔术方法)
在 toString 方法中,通过 arguments.callee.caller 获取到调用该方法的函数的引用,并利用该引用获取到 process 对象,然后使用 child_process 模块的 execSync 方法执行命令 whoami,并将结果转换为字符串返回。实现了RCE
当然,有时即使我们重写了toString函数,但沙箱外并不存在去触发该函数的相关操作或根本不能重写函数,这时我们可以用Proxy来劫持属性:
1 | |
参考文章:
https://xz.aliyun.com/t/11859
https://blog.csdn.net/m0_62422842/article/details/128553953
[Proxy 和 Reflect - 掘金 (juejin.cn)](https://juejin.cn/post/6844904090116292616)