[译]Overflow三部曲

Posted by Cai Qiqi on 2016-10-13

[译]Overflow三部曲

在Web安全领域,我们并不会每天都碰到overflow这个术语。然而当我在用fiddler对twitter抓包的时候,我想要分享三个跟overflow有点关系的趣事。

HTTP响应头拆分造成的overflow

这个问题清晰地展示了HTTP响应拆分与CRLF注入的区别。正常情况下,绝大部分HTTP响应拆分vulnerabilities是由CRLF注入造成的,然而这一个vulnerability运用了一个并不涉及CR或者LF的缺陷。

这得从上一节开始说起。他们的monkey-patch方法终于有效果了。然而,对同一个页面的输入仍然缺乏恰当的验证,于是就导致了这个问题。当我们在一个输入的参数中构造一个足够长的值的时候,HTML源码会直接被展示出来。

这不寻常的情况是由于缺少Content-Type字段造成的。事实上,随着HTTP响应应该一起被发送的响应头字段中,有一半都被分成了两半。服务端好像只会处理响应头的前8kb,然后当它发现了响应很可能不完整或者已经损坏了,它就返回一个HTTP 500(Internal Server Error)。有趣的是,被截断的部分被返回到了客户端,并且于此同时会再次发送相同的请求(就叫它第二次响应吧)。鉴于我们可以控制在响应头中返回的内容,我们可以注入一个字符串,使得这个字符串加上之前的响应头刚好超过8kb一点点,然后突然之间,我们注入的字符串中的最后一对字符变成了下一个响应头的开始。

在这种情况下,当我们对report_user_id注入了如AAA[...]AAA:foobar这样的字符串之后,AAA:foobar就变成了第二个响应中一个叫AAA的,值为foobar的响应头字段。很显然,在实际的攻击中,我们可以将其替换成一个标准的响应头字段(比如,Access-Control-Request-Origin)。

然而,由于有时候响应头会发生改变,我们难以确定应填充多少才可以恰好填满8kb。因此我们需要一种确定的方法能让它在我们想要的任意位置截断。于是我快速做了一下模糊测试,发现服务端会自动去掉响应头字段名中的空格(U+0020)。也就是说,我们可以用空格来填充这8kb。于是我总结如下:

  1. 在请求头前面放一堆空格(要略微少于8kb,否则服务端会响应HTTP 414(请求的URL过长)拒绝此次请求),以作攻击之用
  2. 将构造好的值放到其中一个输入参数中
  3. 使受害者访问这个页面两次,因为实际上只有第二次才真正触发这个payload

最后还有一个难点,就是浏览器会将空格编码为%20。这个问题在于,这会用掉三个字节,而不是一个。这样就使得我们意图构造的payload过长(还记得我之前说的请求URI要比8kb小吧)。幸运的是,加号(+)也是可以视为编码过的空格来处理的。
说实话我也不知道造成这个问题的根源到底是什么。也许是一个缓冲实现的bug,但我也不确定。总之不管怎么样,这个奇怪的行为被修复了,但是它对于输入仍然没有做验证处理。

最终的攻击payload

https://twitter.com/i/safety/report_story?next_view=report_story_start&source=reporttweet&reported_user_id=1&reporter_user_id=1&is_media=true&is_promoted=true&reported_tweet_id=+++[...]+++set-cookie:foobar

完整的响应头:

HTTP/1.1 200 OK
set-cookie: foobar
x-response-time: 229
x-connection-hash: 4f7c08fce85fe4801b3b24f05764fc84
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-transaction: f9709a489ba395b5
x-twitter-response-tags: BouncerCompliant
x-xss-protection: 1; mode=block

参考

HackerOne的原始报告

Cookie炸弹拒绝服务

上面的发现实际上只是这一个的副产品。既然输入是反映到Set-Cookie的,我们可以控制这些特定cookies的值和它们的属性(比如 Domains, Expires)。先前这意味着CSRF防护绕过(用逗号(,)作为cookie的分隔符,不管有没有用吧),但是现在已经不是了,因为Twitter现在会把这个token和在seesion中的做比较。然而,攻击者仍然可以用Cookie炸弹来进行利用。
『Cookie炸弹』这个术语是由Egor Homakov提出的。这种攻击本身并不新鲜,但其实很少有人研究它。其核心要点在于服务端会拒绝异常长的header。准确的数字可能会因不同的服务器而不同,但是通常来说请求头不能超过8kb。通过运用这个特性,攻击者可以强制受害者接受非常大的cookies。随后,来自受害者的去往相关网站的所有请求将包含一个非常长的cookie,使得服务端拒绝来自受害者的任何请求(也叫做拒绝服务)。

这个攻击可以通过操作不同的cookie属性来做进一步的改善:

  • Domains: 并不是只有example.com, 其实.example.com这种域会使cookies应用到其所有的子域中, 以致瘫痪整个服务
  • Expires: 默认情况下,除非特别指定,一个cookie会在浏览器重新启动之后被销毁。反过来攻击者也可以利用这一点将受害者永远锁在门外,直到他们手动删除cookies
  • Path: 将其设置为根路径(/)可以使受影响的页面最大化

实施攻击

攻击payload(为了方便阅读没有进行编码):
https://twitter.com/i/safety/report_story?next_view=report_story_start&reported_user_id=000[...]000;Expires=Wed, 02 Apr 2025 12:21:55 GMT;Path=/;Domain=.twitter.com&reporter_user_id=1&is_media=true&is_promoted=true&reported_tweet_id=000[...]000;Expires=Wed, 02 Apr 2025 12:21:55 GMT;Path=/;Domain=.twitter.com

响应头:

HTTP/1.1 200 OK
[...]
set-cookie: reported_user_id=000[...]000;Expires=Wed, 02 Apr 2025 12:21:55 GMT;Path=/;Domain=.twitter.com
set-cookie: reported_tweet_id=000[...]000;Expires=Wed, 02 Apr 2025 12:21:55 GMT;Path=/;Domain=.twitter.com
[...]

你可能想问为什么要用两个cookies,而不仅仅是一个。那是因为单个cookie只能包含最多4kb的数据。

在受害者访问了这个页面之后,我们的炸弹就默默地埋下了。不用去管它们了。

总结

还是再说一遍,黄金法则就是,一定要验证用户的输入,包括长度。幸运的是这次Twitter对输入进行了恰当的验证。另外要说的是,你要知道你不能让用户设置任意的cookie值,这也很重要。

基于DOM的Cookie炸弹

在DOM中关于cookie的API真是一团糟。因为只有一个属性可以使用(也就是document.cookie)。开发者需要另外费心思来确保其被正确使用。我想在这里演示另外一个Twitter的Cookie炸弹。

分析

Twitter曾经通过cookies来记录hashtags和referers。它们是这样做的:

function d(a) {
[...]
var b = document.referrer || "none",
d = "ev_redir_" + encodeURIComponent(a) + "=" + b + "; path=/";
document.cookie = d;
[...]
}
[...]
window.location.hash != "" && d(window.location.hash.substr(1).toLowerCase())

简单来说,如果访问者来自其他站点,它会从document.hashdocument.referrer中记录下hashtag到一个cookie中。其罪魁祸首是,这个cookie的值(referrer)并没有进行编码或者验证,这就允许攻击者可以设置任意的cookie值。需要知道的是,浏览器会对在document.referrer中的特定字符进行编码。为了使影响最大化,我们需要能够注入一些关键的字符,也就是分号(;)和等号(=),用来操作cookie的属性。所有的浏览器似乎都对它俩保持原样,而会对空格进行编码。如果不对空格进行编码,我们将无法对Expires指定一个有效的GMT日期(Wdy, DD-Mon-YYYY HH:MM:SS GMT)。虽然我们可以用Max-age来告诉浏览器多少秒之后cookie过期,但是有些浏览器(比如IE)并不能识别这个属性。

下面的表格展示了在各个浏览器中,Expires 中哪些字符可以替代空格

  • Chrome: 33 (!), 34 (“), 35 (#), 36 ($), 37 (%), 38 (&), 39 (‘), 40 ((), 41 ()), 42 (*), 43 (+), 44 (,), 45 (-), 46 (.), 47 (/), 60 (<), 61 (=), 62 (>), 63 (?), 64 (@), 91 ([), 92 (), 93 (]), 94 (^), 95 (_), 96 (`), 123 ({), 124 (|), 125 (}), 126 (~)
  • Firefox: 9 (TAB), 40 ((), 41 ()), 44 (,), 45 (-), 47 (/), 91 ([), 93 (])
  • Internet Explorer: 除了字母数字外的任意字符
  • Sarafi: 9(TAB)

所以大部分浏览器都会接受这种奇怪的日期格式。你可以这样测试: document.cookie = 'foo=bar; expires=Thu,01[Jan/2025^00:00:01+GMT'

最终的攻击步骤

  1. 准备好一个这样URL的页面 https://attacker.com/?AAA[...]AAA;domain=.twitter.com;expires=Thu,01[Jan/2025^00:00:01+GMT
  2. 将受害者重定向到 https://twitter.com/#foohttps://twitter.com/#bar 以设置两个大cookies(由于每个cookie有4kb大小的限制)
  3. 恭喜受害者,我们治好了他们的Twitter瘾!

学到的东西

DOM中的Cookie的API很危险。也许该有个合适的API了?

说在最后的


验证用户的输入可不是开玩笑的。即便是一个冗长的输入也可以要了你应用的命。


原文:http://blog.innerht.ml/page/7/