(此漏洞厂商已解决)
缘起
我在公司是做应用安全的,平时主要关注于“防”。由于工作繁忙,少有时间来尝试攻击别人的系统。但是攻防本一家,做了那么多防治安全问题的解决方案,也该试试手,把防护经验用在攻击中,看看效果如何?
第一个问题,进行什么类型的攻击?时间有限,已经是深夜了,为了节省时间,还是选跨站脚本注入吧。遍地都是XSS,哪个上规模的网站上没有XSS问题反倒怪了。第二个问题是,选什么网站?XSS漏洞分布广,攻击门槛低,为了保证有一定的挑战性,我选择了淘宝。第三个问题,攻击哪个页面的哪个字段?简单起见,就选择首页的第一个输入框,商品搜索。
好,动手。我打开IE来到淘宝。
投石问路
首先,我在商品搜索框中输入swx<>&"'。其中字串swx是为了方便在页面输出中查找注入的内容,后面的几个乱字符,则是HTML中可能会引起跨站脚本的常见敏感字符。点击搜索,结果页面出来了,没有什么语法或者显示上的错误。有一个现象,就是页面的标题,以及页面最上方的搜索框里面,搜索关键字已经变成了swx<>&"',除小于号之外的四个字符都被HTML编码了。这说明开发人员有点心虚,或者缺乏统一的编码解决方案,导致在不同的地方编码了两次,多余了,不影响安全,但是改变了用户的搜索关键字。
第一步注入没有明显结果很正常,只是扔几个怪字符过去,探探路而已。第二步要做的事情是分析HTML的源代码,看看我们给的那几个探路的字符,到了敌人那里下场如何?
蛛丝马迹
选择View Source,打开HTML源代码,然后搜索swx关键字,UltraEdit显示有71处发现,还好不算太多,一个一个看。基本上,注入的那几个特殊字符在页面的各处都被施加了HTML编码,包括head里面、HTML body里面、JavaScript字符串值里面等等。有些地方还确实被HTML编码了两次,如前面所推测。
值得注意的是,单引号则始终没有被编码,这是唯一保持原样的字符。但是因为页面的开发人员在HTML里面和JavaScript里面始终使用双引号来包住字符,单引号无法逃出双引号构成的牢笼,也就不能产生语法意义了。
其实直接输入HTML标签的跨站脚本攻击,比如<script>alert(1)</script>,成功的可能性比较小,除非目标网站太烂了。一般只要开发人员稍微考虑一下安全问题,都会把<>这样最基本的危险字符干掉。
页面里用的都是HTML编码,包括JavaScript代码里面也是。而JavaScript编码似乎被忽略了,所以我把目光移向JavaScript里面注入的那段内容:
var word = "swx<>&"'";
单引号无辜的呆在那里,似乎在等待援军。看来得尝试更多的特殊字符,目标,JavaScript。
掩护
第二波,我只输入了一个反斜杠字符\。反斜杠是JavaScript里面字符串常量非常重要的一个字符,也就是编码字符。因为JavaScript字符串可以用被单引号或者双引号括起来的,并且只能是单行的。但是如果字符串的内容本身就需要包含引号或者换行符怎么办?这时就需要进行编码,比如双引号可以被编码为\x22或者\"。其中22是双引号的ASCII编码值的16进制形式。
在商品搜索栏输入\。点击搜索按钮,出问题了。结果页面中有JavaScript语法错误:Unterminated String Constant,未结束的字符串常量。
Bingo!
其实这么简单的case淘宝没有测试到,实在是不应该。一个简单有效的测试做法是,往输入框里扔上一堆乱字符~`!@#$%^ *()_+-={}|[]\\x22:”;’<>,.?/*,如果在结果页面中,这些字符都能原样的显示在界面上,没有任何语法报错或者页面显示上的异常,那么基本上可以说跨站脚本的可能性就比较小了,包括SQL注入漏洞也顺带的被验证了。
第二次注入尝试后,HTML代码里是这样的:var word = "\";。显然,反斜杠兄弟成功打入敌人内部。后面紧跟着的双引号和反斜杠变成了一个整体的编码\",双引号被吃掉了,从而原来用以包住字符串的一对引号只剩下了前一个,所以页面报“未结束的字符串常量”错误。正因为反斜杠的特殊性,任何字符都可以被它所编码。任何字符,当然也包括大于号小于号这一对XSS攻击最基本的字符。
现在,我们试一下,利用JS的这个字符编码机制,让反斜杠掩护刚才被干掉的大于号、小于号和双引号字符潜入到word变量的值里面,一旦这三个字符可以潜入敌后,那么脚本执行也就有了希望。我们想赋给word变量的值是<>",用反斜杠编码掩护之后,变成\x3c\x3e\x22,注意,这里之所以用16进制ASCII码的编码方式,是因为\"这样的编码中双引号还是以明文出现,会被Web服务器上的编码逻辑发现并干掉。而\x22看起来就非常像良民,逃过检查的可能性比较大。
输入\x3c\x3e\x22,点击搜索,然后在结果的HTML文本里查找word =得到:
var word = "\x3c\x3e\x22";
三个特殊字符可以注进去,这意味着,现在我们已经可以在word变量的值里面任意写HTML标签代码,比如类似<script>alert(1);</script>这样的。另一个有价值的发现,就是把页面卷屏到一半的位置时,屏幕右方的显示出现混乱,本来应该是有问题就打听一下的栏目标题,变成了" target="_blank">有问题就打听一下,说明HTML语法已经被破坏。JS异常和页面显示错乱都很可能意味着XSS,因为这两个现象都说明输入的特殊字符已经起了某些语法上的作用,而不仅仅是当做字符显示或者保存在HTML中。
侦查
存在跨站脚本注入漏洞的可能性看来很大,但是为了构造有效的攻击字串,我们需要跟踪分析word变量的使用。通过View Source Code,查找跟踪word变量的值的传播和使用路径,得到以下代码,注释是我加的 (网站开发人员可不会主动在注释里面写明漏洞):
// 注入内容原样出现在JS代码的字符串常量中,但是在赋值给word时
// JS引擎会进行解码,所以word变量的值是<>"三个字符
var word = "\x3c\x3e\x22";
…
//word变量的值直接拼到了moreDating后面,是个URL
var moreDating="http://dating.xxx.com/search_question.htm"+"?q="+word;
…
var _hebin = function(a, b){
...
if (a != 0) {...}
_html += '...美容达人心得榜</a></div>';
else {
// moreDating URL直接作为<a>标签的href属性值
// 显然,这里只要moreDating变量的值包含一个小于号(已经有了!)
// 就可以强行终止<a>标签,然后后面就可以自由书写自己的脚本标签了
_html += '<div class="Title"><a href='+moreDating+' target="_blank">有问题就打听一下</a></div>';
…
}
…
if(a>0||b>0){
layer.innerHTML += _html;
}
阅读过这段代码,基本上可以确定跨站脚本攻击的整个流程是通的。用户可以输入经过JS编码的HTML标签,然后被解码并存储到word变量,word变量又被拼接到moreDating这个URL的后面作为参数,而moreDating在满足特定条件(a等于0而b大于0)时,会被拼装到HTML里面去,并输出到页面。
_hebin这个函数在什么条件下会被触发?粗略读了一下代码之后,发现逻辑不是很清晰。算了,先不浪费时间来研究这个。让事实说话吧。
发动
上一节的那段代码转了两道弯,实际上可以等价简化为下面这样:
<a href=http://.../?q= +word+ target="_blank">。其中word变量的前后分别被拼上了一小段HTML代码,然后被输出到页面。如果我们能通过适当设置word的值,让这一行代码变成下面这样:
<a href=http://xxx/?q=><img src=javascript:alert(1) target="_blank"> 那么JS脚本就会被执行了。大家也许发现后面那个看起来多余的target=,这其实不要紧,只是给HTML标签赋予一个无意义的属性值。上面字串的中间黑体字部分就是word变量应当具有的值,但是我们不能直接输入它,那样会被HTML编码逻辑捕捉到。把这个值用JS编码过后,就可以躲过HTML编码,得到我们需要输入的搜索关键字的值为:
\x3e\x3cimg src=javascript:alert(1) 我把这段文本拷贝出来,贴到商品搜索输入框,点击搜索按钮。等了几秒钟后,“当”的一声响,警告框弹了出来。成功了。对应上面的搜索关键字的URL是:http://search1.taobao.com/browse/0/n-g,lr4dgzk4pazwg2lnm4qhg4tdhvvgc5tbonrxe2lqoq5gc3dfoj2cqmjj-------2-------b--40--commend-0-all-0.htm?at_topsearch=1&ssid=s1-e
直接访问这个URL,也会弹出警告框。用这个漏洞可以干些什么?那就看读者的想象力和JavaScript编程的功力了。
后来经过验证,这个攻击在FireFox里没有效果,只有在IE里可以弹出对话框。具体原因我也懒得去深究了。实际攻击时也没有上面写的这么顺利,我先试<script>标签,注入了但是没执行,再试<a onmouseover=,可以执行,但是onmouseover触发不好用,最后试<img src=才获得了理想的结果。另外,注入的脚本要生效还有一个条件(大概是,不完全确定),就是输入的关键字,在“打听一下”里面必须要能搜到结果,并且在“美容达人心得榜”里面必须搜不到结果,只有在这两个条件都满足时,“打听一下”的界面才会显示出来,从而注入JS才会被实际插入到页面的HTML代码里并触发执行。如果想尝试用其他的注入语法或者注入其他的JS语句,这个问题需要注意一下。
回顾
淘宝的开发人员在页面里只使用了HTML编码,并且只编码了<>&"这几个最常见的特殊字符,这是上面这个漏洞的主要原因。这似乎也是一个在Web开发安全编程中比较常见的误解。其实要全面防治跨站脚本,仅仅对这个几个特殊字符进行HTML编码是远远不够的,JavaScript、URL、CSS、字符集、Web服务器、浏览器环境等等,任何一个环节的疏漏,都可能会引起跨站脚本注入问题。而需要编码的可能有危险的字符,当然也远远不止那几个。
一个广泛推荐的做法是,使用白名单进行字符编码。白名单的意思就是说,除了字母数字这些个无论何时也没有特殊语法含义的普通字符之外,其他所有的特殊字符都进行编码。假如淘宝的开发人员采用了白名单编码方法,就算他们没有意识到JavaScript需要自己特有的编码逻辑,也可以防治前面演示的攻击。因为反斜杠会被HTML编码为\,导致var word = "\x3c\x3e\x22";会变成var word = "\x3c\x3e\x22";。反斜杠在JS代码里消失了,大于号等字符被伪装,但是再也脱不下伪装,无法还原并实施攻击了。
对于那些逻辑简单的网站,白名单方式的HTML编码算是比较简单有效的跨站脚本解决方法。但是要彻底解决问题,尤其是对于功能复杂,有大量混合使用JavaScript、HTML标签、事件处理等语法元素的Web应用程序,如果不做好正确的、足够的编码,仅仅依赖于白名单方法,毕竟不是正道,总还是有缺陷的。下面就有个例子。
一点引申
<script>function showDetails(param) {}</script>
<body onload="showDetails('');alert('1')">Tom</body>
这是一段HTML代码,粗体字部分是用户输入的一些数据,已经被进行了白名单方式的HTML编码,也就是说所有的特殊字符都被编码了。但是这里面还会存在跨站脚本问题吗?读者可以把这段代码另存到一个HTML文件里面,用浏览器执行试试看结果,想想为什么。如果有机会,再和大家多谈谈跨站脚本攻防的话题。
如对本文有疑问,请提交到交流论坛,广大热心网友会为你解答!! 点击进入论坛