7f3c6b234a0a4177e0235f746e8b284d.jpg

前些日子Intigriti出了一道关于XSS的题目。目前比赛已经结束了,但是仍可以通过下面地址体验一下:
https://challenge.intigriti.io

题目很简单,就是从上图中的代码中找到xss漏洞,即可获胜

分析

分析一下这段代码,为了方便测试,我把这个xss挑战代码放到了我本地http://127.0.0.1/xssctf/index.html地址

首先在第二行处有个一个白名单,如下图红框处

7cfcb028a601cb6d3e6e4278adbcfbb8.png

Whitelist中定义了两个域名,’intigriti.com’与’intigriti.io’。这个白名单在下文代码中会用到

接着通过location.hash.substr(1)取出字符串,并生成一个URL对象赋给url变量

033bf0112f5402dc2cb1ba95fed6475a.png

window.location.hash获取url中的锚点,简单来说就是url中#号以及#号后的部分

例如访问:http://127.0.0.1/xssctf/hash.html#hashtest

hash.html代码如下

e62dc97c301f191973dd53600d021165.png

控制台输出如下

027e0dfd09d30936e54fe104cc314388.png

值得注意的是:锚点中的内容仅供浏览器定位资源,并不会随着请求发送给目标服务器的

注意下图,请求中并不包含锚点#hashtest内容

9024099cefc4390d08d1a7f77005c855.png

location.hash.substr(1)实际上是用来获取井号之后的字符串,如上例获取的字符串则是hashtest

接下来看原题

1003f667dd93047f53b91d87daa79ff5.png

程序在上图第四行红框中检查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下来,如下图

f6ff03708da5bffd8b1ab6a1c29c29d8.png

上图第5行处,由于这里url.href被编码,导致无法利用,所以这里不存在利用点

接着看下面这块

520e7ffd4da03d8f550b4267ebf9b843.png

在上图红框处,存在一个setTimeout方法,setTimeout方法用于在指定的毫秒数后调用函数或计算表达式,是一个计时器

当计时结束后,执行上图第7行内容,再次使用location.hash.substr(1)读取锚点值并跳转。

如果此时的锚点值为恶意的xss payload例如javascript:alert(document.domain),跳转时则会产生xss漏洞

那么如何使得程序在第三行处获的锚点值为白名单中的’intigriti.com’或’intigriti.io’

但在第7行时再次获取时变成恶意的xss payload呢?

其实思路有两种,这就对应了这道题的两个不同解法

  1. 计算好时间,设计一个延时引信,等第一处location.hash.substr(1)一执行完就该锚点值

52f83caa986cb944b54d5185b7998979.png

这种思路只需要关心程序从加载到执行完上图红框处需要多少时间

  1. 等这个脚本加载完毕,但setTimeout计时器时间未到,还未来得及执行第二处location.hash.substr(1)时修改锚点

9c14b7769a38d4a88bd3832020d57896.png

这种思路只需要关心程序从执行上图setTimeout计时器使计时器开始运作时,到脚本完全执行结束需要多少时间。后续会介绍为什么需要考虑这个时间

如果想实现以上两种中途修改锚点的操作,可以使用HTML内联框架元素(<iframe>)。它能够将另一个HTML页面嵌入到当前页面中,当把我们的目标页面嵌入我们构造的恶意页面中,我们就可以通过修改src属性来修改锚点的值

解题思路

解法一

我将本次题目代码放到了http://127.0.0.1/xssctf/index.html ,下文里这个地址对应的就是这次题目在我本地的地址

643426f63105138270301a9d3d072ce6.png

解法一是通过估算出从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

但是这个时间并不是很好计算,不能太早,也不能太晚

c4cded328593c2b4b3367d1d171ad0ff.png

上图红框处的200,只是一个示例值,实际中不一定是这个数字,具体算起来麻烦且很不稳定

解法二

在看第二种解法之前,先介绍一下onload事件

onload 事件会在页面或图像加载完成后立即发生

但从字面意义上,有点难以理解具体onload的执行时机:

例如通过iframe嵌入一个页面,被嵌入的页面中有一个setTimeout计时器,onload是等待计时器计时结束,并执行完计时器内部的代码后再执行呢?还是

等被嵌入的页面逐行执行结束,不需要等待被嵌入页面的计时器计时结束,执行执行。

为了测验onload加载的时机,我做了如下实验

Loadiframe.html 使用iframe来嵌入其他页面,代码如下

d53edeeacb93c137b503063f18b213be.png

Iframe.html 被嵌入的页面,其中有一个setTimeout计时器,计时5秒之后在控制台里打印”step 4”

ca858cc9ccfb155c89833d768f012c10.png

实际结果如下:

d09c0c5a62c0b6d0df37111ca6d18eab.png

当Iframe.html中脚本被逐行加载完后,Onload并未等待step 4 打印,直接触发

通过这个例子可以看出,当被加载的页面中存在计时器时,并不影响onload的执行。

只要被加载的页面中脚本被逐行加载完毕,onload就触发。

因此对应的解法如下

a4eb0c2c89f3859e5de62e36a8434b33.png

程序执行到上图11行处时,index.html开始加载

当index.html中脚本加载到下图第六行时,setTimeout计时器开始计时,在计时结束后执行location
= location.hash.substr(1);

c8197ce9964bf55b6f99df9081fec35f.png

随后,脚本逐行执行。当index.html中脚本全部被执行完成后,恶意构造的页面中的onload被触发

334dea30f093e7b1f82e2a3a2038c6fc.png

上图红框处即为onload触发时的代码,此处代码修改锚点值,将其变为恶意的payload

这种解题思路的核心在于,从index.html中setTimeout计时器启动开始,执行index.html中后续的代码的时间,即加载完毕index.html从而可以触发onload事件的时间,与setTimeout计时器设置时间的比较

8c02c6d8f97340b5b6042c49c7a862f7.png

当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并没有设置计时时间,采用默认值

默认计时时间是多久呢?我查看了下相关资料

41e629b599ba7c85515335076736935c.png

555df4d09d621fea8a08cb206353818d.png

可见如本次题目中这样,并不设置计时时间,留给我们的最少有4ms

实际上,我测了一下在我的环境下,这个时间大概多久。使用如下代码

138620a7c0a940b191e0547c7a887dbe.png

b66663e54b9f3b29e2f4ea731f7baeb2.png

尽管测试代码不是很精准,但大概有59毫秒

而执行index.php后续的仅剩的几行代码,几乎用不了这么多时间

在0.059秒内,只要后续几行代码被执行,onload顺利触发,锚点被修改,我们就可以结题成功了

关于其他解法的一些想法

我发现,有些师傅使用如下思路

f5378d820f0f6ae1de8efd687545374e.png

想通过超长的锚点值来拖延目标程序运行时间

我们再来看下原题

2dd50a171220bc768ad3927d91fc2233.png

既然这种解法是通过onload来改变锚点值,那么影响结果的因素如上文分析,只有setTimeout的计时时间和setTimeout计时开始后,执行完后续代码以至onload触发的时间

这里的超长锚点值,并不能影响这两者中的任何一个,因此我私自认为应该是没有用的,欢迎感兴趣的师傅一起讨论。