前些日子Intigriti出了一道关于XSS的题目。目前比赛已经结束了,但是仍可以通过下面地址体验一下:
https://challenge.intigriti.io
题目很简单,就是从上图中的代码中找到xss漏洞,即可获胜
分析
分析一下这段代码,为了方便测试,我把这个xss挑战代码放到了我本地http://127.0.0.1/xssctf/index.html地址
首先在第二行处有个一个白名单,如下图红框处
Whitelist中定义了两个域名,’intigriti.com’与’intigriti.io’。这个白名单在下文代码中会用到
接着通过location.hash.substr(1)取出字符串,并生成一个URL对象赋给url变量
window.location.hash获取url中的锚点,简单来说就是url中#号以及#号后的部分
例如访问:http://127.0.0.1/xssctf/hash.html#hashtest
hash.html代码如下
控制台输出如下
值得注意的是:锚点中的内容仅供浏览器定位资源,并不会随着请求发送给目标服务器的
注意下图,请求中并不包含锚点#hashtest内容
location.hash.substr(1)实际上是用来获取井号之后的字符串,如上例获取的字符串则是hashtest
接下来看原题
程序在上图第四行红框中检查url中的hostname属性,是否在whitelist中
因此我们如果想让程序进入此处if分支,需要构造#http://intigriti.io 类似的锚点值
对应的url应该类似http://127.0.0.1/xssctf/index.html#http://intigriti.io
在进入if分支后,程序接着将url.href通过encodeURIComponent编码,并document.write下来,如下图
上图第5行处,由于这里url.href被编码,导致无法利用,所以这里不存在利用点
接着看下面这块
在上图红框处,存在一个setTimeout方法,setTimeout方法用于在指定的毫秒数后调用函数或计算表达式,是一个计时器
当计时结束后,执行上图第7行内容,再次使用location.hash.substr(1)读取锚点值并跳转。
如果此时的锚点值为恶意的xss payload例如javascript:alert(document.domain),跳转时则会产生xss漏洞
那么如何使得程序在第三行处获的锚点值为白名单中的’intigriti.com’或’intigriti.io’
但在第7行时再次获取时变成恶意的xss payload呢?
其实思路有两种,这就对应了这道题的两个不同解法
- 计算好时间,设计一个延时引信,等第一处location.hash.substr(1)一执行完就该锚点值
这种思路只需要关心程序从加载到执行完上图红框处需要多少时间
- 等这个脚本加载完毕,但setTimeout计时器时间未到,还未来得及执行第二处location.hash.substr(1)时修改锚点
这种思路只需要关心程序从执行上图setTimeout计时器使计时器开始运作时,到脚本完全执行结束需要多少时间。后续会介绍为什么需要考虑这个时间
如果想实现以上两种中途修改锚点的操作,可以使用HTML内联框架元素(<iframe>)。它能够将另一个HTML页面嵌入到当前页面中,当把我们的目标页面嵌入我们构造的恶意页面中,我们就可以通过修改src属性来修改锚点的值
解题思路
解法一
我将本次题目代码放到了http://127.0.0.1/xssctf/index.html ,下文里这个地址对应的就是这次题目在我本地的地址
解法一是通过估算出从http://127.0.0.1/xssctf/index.html 被加载到第一处location.hash.substr(1)执行时的时间,通过估算这个时间(用X表示),只要在这个时间之后,最理想的是执行后的一瞬间(因为再晚了第二处location.hash.substr(1)执行可就执行了),
通过
document.getElementById(‘xss’).src把锚点改为#javascript:alert(document.domain)
这样一来,当第二处location.hash.substr(1)执行时,获取的锚点值就会是#javascript:alert(document.domain) ,从而导致xss
但是这个时间并不是很好计算,不能太早,也不能太晚
上图红框处的200,只是一个示例值,实际中不一定是这个数字,具体算起来麻烦且很不稳定
解法二
在看第二种解法之前,先介绍一下onload事件
onload 事件会在页面或图像加载完成后立即发生
但从字面意义上,有点难以理解具体onload的执行时机:
例如通过iframe嵌入一个页面,被嵌入的页面中有一个setTimeout计时器,onload是等待计时器计时结束,并执行完计时器内部的代码后再执行呢?还是
等被嵌入的页面逐行执行结束,不需要等待被嵌入页面的计时器计时结束,执行执行。
为了测验onload加载的时机,我做了如下实验
Loadiframe.html 使用iframe来嵌入其他页面,代码如下
Iframe.html 被嵌入的页面,其中有一个setTimeout计时器,计时5秒之后在控制台里打印”step 4”
实际结果如下:
当Iframe.html中脚本被逐行加载完后,Onload并未等待step 4 打印,直接触发
通过这个例子可以看出,当被加载的页面中存在计时器时,并不影响onload的执行。
只要被加载的页面中脚本被逐行加载完毕,onload就触发。
因此对应的解法如下
程序执行到上图11行处时,index.html开始加载
当index.html中脚本加载到下图第六行时,setTimeout计时器开始计时,在计时结束后执行location
= location.hash.substr(1);
随后,脚本逐行执行。当index.html中脚本全部被执行完成后,恶意构造的页面中的onload被触发
上图红框处即为onload触发时的代码,此处代码修改锚点值,将其变为恶意的payload
这种解题思路的核心在于,从index.html中setTimeout计时器启动开始,执行index.html中后续的代码的时间,即加载完毕index.html从而可以触发onload事件的时间,与setTimeout计时器设置时间的比较
当setTimeout计时器设置时间比执行完上图红框处代码所需的时间长,则可顺利触发onload将锚点值更改,在计时结束后,计时器中代码获取的锚点值就变成了我们的payload
但是如果setTimeout计时时间很短,index.html后续代码还未加载完,onload还未来的急触发,那利用就会失败
举个例子:
setTimeout设置的是5000,也就是5秒。当setTimeout这行代码被执行,计时开始后,如果index.html中剩下几行可以在5秒内执行完,onload如期执行,5秒后的location.hash.substr(1)就变成了javascript:alert(document.domain)
但是挑战中的setTimeout并没有设置计时时间,采用默认值
默认计时时间是多久呢?我查看了下相关资料
可见如本次题目中这样,并不设置计时时间,留给我们的最少有4ms
实际上,我测了一下在我的环境下,这个时间大概多久。使用如下代码
尽管测试代码不是很精准,但大概有59毫秒
而执行index.php后续的仅剩的几行代码,几乎用不了这么多时间
在0.059秒内,只要后续几行代码被执行,onload顺利触发,锚点被修改,我们就可以结题成功了
关于其他解法的一些想法
我发现,有些师傅使用如下思路
想通过超长的锚点值来拖延目标程序运行时间
我们再来看下原题
既然这种解法是通过onload来改变锚点值,那么影响结果的因素如上文分析,只有setTimeout的计时时间和setTimeout计时开始后,执行完后续代码以至onload触发的时间
这里的超长锚点值,并不能影响这两者中的任何一个,因此我私自认为应该是没有用的,欢迎感兴趣的师傅一起讨论。