文章来源:https://mahmoudsec.blogspot.com/2019/04/handlebars-template-injection-and-rce.html


前言

我们在一个名为handlebars的JavaScript模板库上发现了一个0day漏洞,这个漏洞可用于获取Shopify Return Magic应用上的远程代码执行权限。

我的心路历程

在2018年10月,Shopify组织HackeOne活动“H1-514”并邀请一些特定的研究人员参与,我是其中之一。在许多Shopify的应用中都包含一个名为“Return Magic”的应用程序,该程序用于自动化完成Shopify客户的退货流程。

查看这个程序,我找到了一个名为Email WorkFlow的功能,使用该功能店铺商家能够定制自动发送给需要退货的客户的电子邮件。用户可以在模板中使用一些变量例如:{{order.number}} ,{{email}}等等。随后,我决定测试该功能是否存在服务端模板注入,输入{{this}} {{self}},然后发送一份测试邮件给我自己,这封邮件内容包含[object Object],这引起了我的注意。

因此,我花了一些时间试图找出这个程序所使用的模板引擎,我搜索了NodeJS模板库上流行的模板库,认为该程序使用的是mustache (后来发现不是)。然后我测试了mustache模板注入,但没有结果,因为mustache应该是一个logicless(无逻辑)模板引擎,无法调用函数。而然,我可以调用一些对象属性例如{{this.__proto__}},甚至是{{this.constructor.constructor}}这样的构造函数。我尝试发送参数值至this.constructor.constructor(),但没有成功。

我承认这里没有漏洞,然后继续找别的bug。似乎上帝一定要我找出该漏洞,我在Shopify的slack频道上看到了一条消息,Shopify要求提交“疑似bug”。如果某人找到一些感觉可以利用的东西,他可以提交给Shopify安全团队,如果团队利用了这个漏洞,报告者能够获取全额赏金。我立即提交我所发现的内容,影响部分写为“可能存在服务端模板注入,这将导致服务器接管¯_(ツ)_/¯”。

两个月过去了,我仍没有收到Shopify关于这个“疑似bug”的任何回应,然后我被邀请至巴厘岛参加Synack主办的黑客活动。在那里我与Synack 红队成员碰面,活动结束后我应该回到埃及去,但在飞机起飞前三个小时我改变了注意,决定再待一段时间,然后飞往日本参加TrendMicro CTF比赛。Synack红队的一些成员也决定延长呆在巴厘岛的时间,其中的一位是Matias,所以我决定与他一起度过这几天。在享受完沙滩和巴厘岛的美景后,我们回到酒店用餐,那时Matias告诉我他曾在一个赏金项目的bug中中用到了JavaScript沙盒逃逸,并确认漏洞。然后,我们整晚都在搜寻对象和构造函数,但是运气不佳,我们无法逃出沙盒。

我脑海中一直浮现出构造函数,我记得曾经在Shopify上找到过模板注入漏洞。我阅读以前的Hackone报告,然后确定模板不是mustanche,我在本地安装mustanche,使用mustanche解析{{this}},返回的内容与Shopify程序不同。我再次搜索流行的NodeJS模板引擎,将那些使用花括号{{}}作为模板表达式的模板下载到本地。其中的一个库是handlebars ,当我解析{{this}}时它返回了[object Object](与Shopify程序的响应相同)。我查看了handlebars的文档,发现该模板并没有很多防护模板注入攻击的逻辑。此时我能够访问构造函数了,于是我决定探究参数是如何传递给函数的。

从文档中我还发现开发者能在模版范围内注册helpers的函数。我们可以像这样{{helper "param1" "param2" ...params}}传递参数至helpers。首先,我尝试发送{{this.constructor.constructor "console.log(process.pid)"}},但只返回字符console.log(process.pid)。我查看了源代码,想弄清楚发生了什么。在runtime.js中,有以下函数:

lambda: function(current, context) {
  return typeof current === 'function' ? current.call(context) : current;
}

这个函数检查当前对象是否为“function”类型,如果是它将调用current.call(context)context属于模板范围),不是则返回该对象本身。

我进一步分析handlebars文档,发现它在helpers中内置了 "with", "blockHelperMissing", "forEach"函数等等。

审计完helpers的内置函数后,我对如何利用helpers的“with”函数有了一些想法。这个函数用于移动调节模板的context(上下文),因此,我能够在自己的上下文执行curren.call(context)。我尝试使用下面这段代码:

{{#with "console.log(process.pid)"}}
  {{#this.constructor.constructor}}
    {{#this}} {{/this}}
  {{/this.constructor.constructor}}
{{/with}}

简单解释一下,将console.log(process.pid)作为当前的上下文传输,handlebars编译器遇到this.constructor.constructor并将其视为一个函数,它将当前的上下文作为函数参数来调用。然后使用{{#with this}}(我们从函数构造函数调用返回的函数),此时console.log(process.pid)应该被执行。

然而这没有起作用,因为function.call()用一个owner对象作为一个参数,所以第一个参数是owner对象,其他的参数是发送给被调用函数的参数。因此,被调用的函数为current.call(this, context)时,上面的payload就可以起作用。

我在巴厘岛呆了两晚然后飞往东京参加TrendMicro CTF。在东京的时候,我的头脑中还是充满着构造函数和对象的影子,我仍在查找沙盒逃逸的方法。

我想到了另一个办法,在上下文中使用Array.map()函数来调用构造函数,但仍失败了,因为编译器总是向我调用的任何函数传递一个别的参数,然后发生错误,因为payload被视为函数参数而不是函数体。

{{#with 1 as |int|}}
  {{#blockHelperMissing int as |array|}} // This line will create an array and then we can access its constructor
    {{#with (array.constructor "console.log(process.pid)")}}
      {{this.pop}} // pop unnecessary parameter pushed by the compiler
      {{array.map this.constructor.constructor array}}
    {{/with}}
  {{/blockHelperMissing}}
{{/with}}

这似乎有很多可以逃出沙盒的方法,但是我还面对一个大问题:无论调用模板内的哪个函数,模板编译器将把模板范围内的Object添加至最后一个参数。

举个例子,如果我想调用constructor.constructor("test","test"),编译器将把它改为constructor.constructor("test", "test", this)再调用,这是因为调用了类似Object.toString()这样的函数,该函数将其转化为一个字符。该匿名函数可能是以下这种形式:

function anonymous(test,test){
[object Object]
}

这将导致错误的发生。

我试了很多方法,但是不够幸运。然后,我决定打开JavaScript文档查阅Object原型,想要找到帮助我实现沙盒逃逸的方法。

点击收藏 | 0 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖