一个硬核的XSS题,收获相当大(还得感谢CrumbledWall师傅点拨了我几次)
开局给了后端的源码,能看见里面用了腾讯的COS对象存储,可以上传文件到上面,docker-compose.yml里面还有一个名为的bot
的服务但没有给出源码,能看出来后端有个接口会去连接这个bot
。
|
|
寻找前端可控点
接下来看前端。首先发现前端有个异常严格的CSP,这里面的nonce
每次刷新都会生成一个新的。
|
|
然后是JS代码,首先发现有几个地方直接输出了HTML(当然有过滤):
name
GET参数,会被document.getElementById("username").setHTML
写到页面中,当然加了个HTML Sanitizer API进行过滤处理(这个似乎是浏览器直接实现的,也不知道内部的处理逻辑,但反正很严格就是了);img
GET参数,会被<img src="${filter(image)}" />
写进页面中,这里的filter过滤了尖括号以及..
,所以想提前闭合写入其他标签是不太可能了,只能给这个图片增减一些属性,再加上CSP的限制导致onerror
这些脚本也会被ban掉,也没有什么利用价值;- 提交上传文件的时候会返回
data
并写进页面,但这里的数据不可控,通过后端代码也能看见没什么用; - Hash判断为
#mycollection
时会输出一个iframe
,但很可惜这个值也不可控,写死在const
里面; - 最后是读取Cookie中flag的值,并且加盐MD5后作为COS的资源链接,输出一个iframe,这个后面再说。
|
|
综上发现只有三个地方是可控的,并且最后一个只能控制MD5。然后继续看后端代码会发现.html的后缀被ban了,因此这个MD5的可控也没办法访问到文件。
于是乎就只剩下这两个GET参数可用了,img
这里由于filter的存在能做的事情也相当有限,尝试了各种各样的XSS Payload都没法整出来,只好作罢。
折腾Sanitizer
折腾了一阵子img之后发现没有什么进展,于是转换思路开始琢磨这个Sanitizer API的过滤有没有遗漏。询问了出题人,得到回答说并不需要绕过Sanitizer:
但其他的点也都没有突破口,于是还是Fuzz了一下哪些标签可用,不会被过滤:
|
|
输出如下:
|
|
一眼丁真发现了<meta>
标签可以用,也就是说可以插入标签实现跳转到指定页面、刷新等操作。结合上传点的代码,发现限制也并不严格。结合这两点,我们可以XSS插入跳转到任意恶意页面的<meta>
标签,而这个页面也可以自己编写然后上传至COS上,以此来Bypass掉CSP。
利用Service Worker劫持请求
然后在这卡住了半天,因为仅仅能够跳转加上普通的XSS也没办法拿到更多的信息(COS和主页也是跨域的,读不到Cookie等信息)。
直到队友提醒了一波可以用Service Worker,瞬间恍然大悟,第一想到的是2020年西湖论剑的那个XSS题(因为是第一次在那碰到这个东西,所以印象极其深刻)。Service Worker可以理解为一个事件驱动的中间人代理,当这个东西被注册之后,对其所管理域下面的所有请求都会被Service Worker截获,随后便可以在里面进行任何非同步操作,再自定义一个返回的对象返回给浏览器。
于是乎,就有了个自动化跳转的劫持请求方案,下面浅浅画了个流程图:
图中首先Bot收到了我们发送的URL并访问,访问到包含XSS的Challenge页面,跳转到COS中存储的恶意页面Evil.html,该页面中包含创建并注册一个Service Worker的JS代码。注册完毕之后页面自动刷新,此时去往COS的流量就已经全部被Service Worker截获,依照攻击者编写的逻辑将访问页面的URL、Cookie等数据传送到攻击者的服务器中。
要注册一个合法的Service Worker需要满足下面几个要求:
HTTPS(避免中间人攻击)
注册的JS Handler URI的MIME类型必须为text/javascript或者application/javascript等JS文件的合法MIME类型,否则会出现下面的错误:
注册的源必须和站点源一致,且注册的域范围最大不能超过JS文件所在的域路径(也就是说不能越到JS文件所在目录的上级目录,只能在其目录或子目录下注册)
根据MDN Manual中给的样例,可以注册一个Service Worker,如下面将/
域下的请求都注册到/evil.js
处理:
|
|
evil.js中监听一个fetch
的事件,每次域中拉取资源都会触发该事件,之后的每一次请求都会把当前请求的Cookie发送到攻击者的服务器中。事件中即可对事件的属性和页面的一些信息进行读取。但需要注意的是,Service Worker无法对DOM进行任何操作。
|
|
每个Service Worker的有效时间为24小时,超时需要重新注册,当然也可以通过下面的代码提前释放所有的SW:
|
|
上传HTML和JS文件
由后端代码可知,会检查上传文件的文件头来判断是否为图片文件,还会检查文件的后缀名。首先判断是否为JPG/PNG/WEBP格式(其实都是判断文件头),随后判断文件后缀。很容易发现这里的文件后缀判断没有考虑大写的情况,因此可以直接绕过。
|
|
对于HTML而言比较好处理,除了大写后缀的情况还有诸如.shtml
、.xhtml
等后缀可用,测试过后发现在COS上只有.shtml
能够解析;对于JS文件而言则只有大写后缀一个处理方法,但JS文件还需考虑脏数据问题,因为文件内容的开头必须是图片的文件头,此时发现JPG和PNG的文件头都含有不可见字符,JS解析会直接报错,只有WEBP文件头可以全为ASCII字符,因此使用WEBP文件头构造一个变量赋值语句或注释即可绕过。
|
|
上传好了文件后,构造请求通过/report
接口发送给Bot,等数据回显就可以了。整个流程如下:
- 由Challenge页XSS跳转到COS上的1.html,用于注册Service Worker,注册完之后会跳转至2.html;
- 2.html的内容为跳转回Challenge页,用于接收Flag。
下面是完整的Payload:
|
|
|
|
|
|
最后在服务器上会收到一个URL,访问即为Flag。直到拿到Flag我才知道,原来主页取Cookie中flag
的值的目的是为了让Bot将Flag弹回来hhh,所以要将页面跳转回主页,触发Bot通过iframe
请求Cookie加盐MD5后拼合得到Flag的图像。