一个JS沙箱逃逸漏洞

翻译文章:https://licenciaparahackear.github.io/en/posts/bypassing-a-restrictive-js-sandbox/

在做一个Bug bounty的项目时,我发现了一个网站,它有一个很有意思的功能:它能让我使用一些用户控制的表达式过滤数据。比如说,我可以输入 book.proce > 100 表达式,使它只展示一些价格高于 $100 的书。直接输入 true 可以列出所有的书,输入 false 就会一本书都不显示。所以我可以知道我使用的表达式是对是错。

这个功能引起了我的注意,因此我尝试着输入更复杂的表达式,比如 (1+1).toString()==="2" (值为 true)和 (1+1).toString()===5 (值为 false)。这显然是 JavaScript 代码,因此我猜测这个表达式在 NodeJS server 中被作为参数传给了一个类似 eval 的函数。到了这里,我感觉我快要发现一个远程执行漏洞了。然而,当我想要测试一个更加复杂的表达式时,它报错了,提示我输入的表达式非法。我猜测这应该不是 eval 函数,而应该是一个 JavaScript 的沙箱。

沙箱都是再一个受控的环境中执行非可信的代码,而这一般都是很难确保不出问题的。大多数情况下,我们都能找到一些方法来绕过沙箱的保护机制。特别是对于像 JavaScript 这样复杂,特性臃肿的语言,沙箱的漏洞可能会更多。这个问题吸引了我的注意,所以我决定花一些时间来打破这个沙箱的防护机制。我学习了 JavaScript 的一些内部机制,用来发现和利用沙箱的 RCE 漏洞。

我首先要确定这个网站使用了什么库来实现的沙箱,因为 NodeJS 中有几十个类似的库,在许多情况下,它们都存在一些漏洞。当然,也有可能这是一个开发人员自己写的库,但是我忽略了这种可能,因为单纯的网站开发人员不太可能花大把的时间来做这种语言底层的事情。

最后,通过分析网站的错误日志,我推断处它们应该使用的是 static-eval ,这是一个不太流行的库(由 substack写的,这个人再 NodeJS 社区中非常有名)。尽管它的文档中写了,它并不是被设计来作为沙箱的,但是我仍然十分确定他在这个网站中被用作了沙箱。

绕过 static-eval

static-eval的基本思想是使用 esprima 库解析 JS 表达式并将其转换为 AST(抽象语法树)。static-eval 通过分析这个AST 对我输入的表达式进行评估。如果发现一些奇怪的东西,函数就抛出异常,我的代码就不会执行。一开始,我有点灰心丧气,因为我意识到沙箱对它所接受的表达式有很大的限制。我甚至不能在表达式中使用 for 或 while 语句,所以做一些需要迭代算法的事情是几乎不可能的。无论如何,我坚持着继续寻找漏洞。

一开始,我并没有发现任何 bug,所以我查看了 static-eval 项目的 commits 和 pull requests 的所有记录。 我发现 pull requests #18 修复了两个沙箱逃逸的 bug,而这正是我所寻找的。 我还发现了这个 pr 作者的博客,在这篇文章里,他深入分析了这个漏洞。同时,我在立即在这个网站中测试这个漏洞,然而,他们使用了一个新版本的 static-eval,这个版本的 static-eval 早就把这个漏洞补上了。我立即尝试在我测试的网站中使用这种技术,但不幸的是,他们使用的是更新的静态评估版本,已经修补了这个漏洞。但是,知道有人发现过这种漏洞,这让我更加自信,所以我一直在寻找绕过它的新方法。

接下来,我深入分析了这两个漏洞,以期望能够为我找到新的漏洞寻找灵感。

第一个漏洞:

第一个漏洞使用了 function constructor 来生成恶意函数。
这种技术经常用于绕过沙箱。例如,大多数通过绕过 angular.js 沙箱来获得 XSS 的方法都使用一些有效载荷,它们最终都会访问和调用 function constructor。下面的表达式用来演示漏洞,能够打印系统环境变量(这应该是不被允许的,因为沙箱应该阻止它):

"".sub.constructor("console.log(process.env)")()

在这段代码中,"".sub 能够生成一个函数对象, 然后它会执行这个函数的构造函数 constructor,constructor 函数在被调用后会返回一个新函数,该函数的代码是 constructor 的参数。它类似于 eval 函数,但它不是立即执行代码,而是返回一个函数,在调用时执行代码。在这段代码的末尾正好有一个 (),这是为了调用这个新生成函数。

除了显示系统的环境变量,你还可以做一些其他的事情。比如说,你可以使用 child_process 模块的 execSync 函数来执行操作系统的命令,并且返回其结果。这段载荷将会返回执行 id 命令后的结果。

"".sub.constructor("console.log(global.process.mainModule.constructor._load(\"child_process\").execSync(\"id\").toString())")()

这段代码使用了跟前面一段代码相同的方法。在这段代码中,global.process.mainModule.constructor._load 的功能与 require 函数相同。正是由于在 constructor 中我不能直接使用 require,因此我不得不使用 global.process.mainModule.constructor._load 这个名字。

此漏洞的是修补方案是禁止对函数对象的属性的访问(通过 typeof obj == 'function' 来实现的):

else if (node.type === 'MemberExpression') {
    var obj = walk(node.object);
    // do not allow access to methods on Function 
    if((obj === FAIL) || (typeof obj == 'function')){
        return FAIL;
    }
点击收藏 | 2 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖