欢迎来到 黑吧安全网 聚焦网络安全前沿资讯,精华内容,交流技术心得!

如何绕过受限JS沙箱

来源:本站整理 作者:佚名 时间:2019-03-07 TAG: 我要投稿

一、前言
在参与某个漏洞赏金计划时,我发现某个站点的功能非常有趣:该站点允许用户使用可控的表达式来过滤数据。我可以使用类似book.price > 100的表达式来显示价格高于100美元的书籍,使用true为过滤器则显示所有书籍,false为过滤器不显示任何内容。因此,我可以知道所使用的表达式结果为true还是false。
该功能成功吸引了我的注意,因此我尝试传入更为复杂的表达式,如(1+1).toString()==="2"(结果为真)以及(1+1).toString()===5(结果为假)。这显然是JavaScript代码,因此我猜测我使用的表达式会传入NodeJS服务器上类似eval的某个函数。此时貌似我找到了一个远程代码执行(RCE)漏洞。然而,当我使用更为复杂的表达式时,服务器返回错误,提示表达式无效。我猜测服务端并没有使用eval函数来解析表达式,而是使用了JavaScript的某种沙箱。
在受限环境中使用沙箱来执行不可信代码通常并不完美。在大多数情况下,我们已经有一些方法能够绕过这种保护措施,以普通权限来执行代码。如果目标环境尝试限制使用像JavaScript之类复杂功能的语言,那么防护起来更难面面俱到。发现这个问题后,我决定花些时间尝试突破这个沙箱系统。我需要了解JavaScript内部工作原理,这样才能有助于查找并利用RCE。
我首先需要确定网站使用哪个库来实现沙箱,因为整个NodeJS生态中有数十个库可以实现该功能,并且在许多情况下这些实现方案并不完美。也有可能目标网站使用了自定义的沙箱,但这种可能性较小,因为开发者需要较多精力才能做到这一点。
最后,我通过分析应用的错误信息发现目标站点使用的是static-eval,这个库没有那么知名(但开发者是substack,是NodeJS社区的一个名人)。虽然这个库最初并不是针对沙箱场景而设计(其实我现在也不了解这个库最开始的使用场景),但文档中的确涉及相关内容。目前,我测试的这个站点的确将该库用于沙箱环境。
 
二、绕过static-eval
static-eval的原理是使用esprima库来解析JS表达式,将其转化为AST(抽象语法树)。给定AST和我们输入的变量对象后,目标会尝试计算表达式。如果目标发现某一点存在异常,那么函数就会失败,不会执行我们输入的代码。因为这一点,最开始时我有点动力不足,因为我发现这个沙箱系统对能接收的数据非常严格。我甚至不能在表达式中使用for或者while语句,因此想执行需要迭代算法的操作几乎无法完成。无论如何,我一直在尝试寻找系统中是否存在任何bug。
粗略分析后我并没有找到任何bug,因此我查看了static-eval GitHub项目的commits和pull请求。我发现其中有个pull请求修复了2个bug,这些bug可以规避沙箱环境,这正是我所寻找的答案。我也发现了pull请求作者发表过的一篇文章,其中深入分析了这些漏洞。因此,我第一时间在目标站点上测试了这些技术,但不幸的是,目标站点使用的是新版的static-eval,已经修复了这些漏洞。然而,当发现有人曾成功绕过沙箱后,我对自己也更有信心,因此开始寻找能规避沙箱的新方法。
随后,我深入分析了这两个漏洞,希望这些漏洞能帮我找到思路,发现该库中的新漏洞。
 
三、分析第一个漏洞
第一个漏洞使用了constructor来构造恶意函数,攻击者经常使用这种技术来绕过沙箱。比如,在绕过angular.js沙箱以获得XSS攻击点的大多数方法中,最终都会使用能够调用constructor的payload。攻击者也常使用这种方法来绕过与static-eval类似的库,如vm2。例如,我们可以通过如下表达式打印出系统环境变量,证实漏洞的确存在(因为沙箱的存在,该操作可能不会成功):
"".sub.constructor("console.log(process.env)")()
在如上代码中,"".sub是获得函数的一个简单方法((function(){})也能实现类似功能),随后再获取该函数的constructor。当调用该函数后会返回一个新函数,该函数具体代码为传入的字符串参数。这类似于eval函数,但并没有立即执行代码,而是返回可以执行代码的一个函数。这就可以解释payload结尾为什么会有(),我们可以通过这种方式来调用该函数。

我们可以执行更多操作,而不单单是打印环境变量。比如,我们可以使用NodeJS child_process模块的execSync函数来执行操作系统命令并返回输出结果。如下payload会返回id命令的输出结果:
"".sub.constructor("console.log(global.process.mainModule.constructor._load("child_process").execSync("id").toString())")()
上面的payload与之前的payload类似,不同点在于所创建函数的具体代码。在该代码中,global.process.mainModule.constructor._load与NodeJS中require函数的功能一样。由于我没注意到的某些原因,函数constructor内部无法使用require,因此我只能使用这种不优雅的表达方式。

开发者通过阻止对函数对象属性的访问(通过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;
    }
这种修复方式非常简单,但也非常有效。由于constructor只能在函数中使用,因此现在我已无法访问该接口。对象的typeof属性无法修改,因此只要是函数,那么typeof必定等于function。我没有找到绕过这种防护的办法,因此我接着分析第二个漏洞。

[1] [2] [3]  下一页

【声明】:黑吧安全网(http://www.myhack58.com)登载此文出于传递更多信息之目的,并不代表本站赞同其观点和对其真实性负责,仅适于网络安全技术爱好者学习研究使用,学习中请遵循国家相关法律法规。如有问题请联系我们,联系邮箱admin@myhack58.com,我们会在最短的时间内进行处理。
  • 最新更新
    • 相关阅读
      • 本类热门
        • 最近下载