Discord桌面应用RCE漏洞分析
几个月前,我在Discord桌面应用中发现了远程代码执行漏洞,并通过其漏洞奖励计划进行了报告。这个RCE漏洞的有趣之处在于它需要结合多个漏洞才能实现。本文将分享详细技术细节。
选择Discord作为目标的原因
我最近想研究Electron应用的漏洞,所以寻找提供Electron应用漏洞奖励的项目,最终选择了Discord。同时作为Discord用户,我也希望确认自己使用的应用是否安全。
发现的漏洞
我基本发现了以下三个漏洞,并通过组合它们实现了RCE:
- 上下文隔离缺失
- iframe嵌入中的XSS
- 导航限制绕过(CVE-2020-15174)
下面逐一解释这些漏洞。
上下文隔离缺失
测试Electron应用时,我首先检查用于创建浏览器窗口的BrowserWindow API选项。通过检查这些选项,我思考当渲染进程中可以执行任意JavaScript时如何实现RCE。
Discord的Electron应用虽然不是开源项目,但Electron的JavaScript代码以asar格式本地保存,只需解压即可阅读。
主窗口使用以下选项:
|
|
这里需要特别检查的重要选项是nodeIntegration和contextIsolation。从以上代码我发现Discord主窗口中nodeIntegration设置为false,contextIsolation也设置为false(使用版本的默认值)。
如果nodeIntegration设置为true,网页的JavaScript可以通过简单调用require()来使用Node.js功能。例如,在Windows上执行calc应用的方式是:
|
|
这次nodeIntegration设置为false,所以我无法通过直接调用require()来使用Node.js功能。
但是,仍然存在访问Node.js功能的可能性。另一个重要选项contextIsolation设置为false。如果要消除应用中RCE的可能性,此选项不应设置为false。
如果禁用contextIsolation,网页的JavaScript可以影响渲染器上Electron内部JavaScript代码的执行,以及预加载脚本的执行(以下将这些JavaScript称为网页外部的JavaScript代码)。
例如,如果从网页的JavaScript使用另一个函数覆盖JavaScript内置方法之一的Array.prototype.join,则当调用join时,网页外部的JavaScript代码也将使用被覆盖的函数。
这种行为很危险,因为无论nodeIntegration选项如何,Electron都允许网页外部的JavaScript代码使用Node.js功能,并且通过从网页中覆盖的函数干扰它们,即使nodeIntegration设置为false,也有可能实现RCE。
顺便说一句,这样的技巧以前并不为人所知。它最早是在2016年由Cure53的一次渗透测试中发现的,我也参与了那次测试。之后,我们向Electron团队报告并引入了contextIsolation。
最近,该渗透测试报告已发布。如果你感兴趣,可以从以下链接阅读:
Pentest-Report Ethereum Mist 11.2016 - 10.2017 https://drive.google.com/file/d/1LSsD9gzOejmQ2QipReyMXwr_M0Mg1GMH/view
你也可以阅读我在CureCon活动中使用的幻灯片:
contextIsolation引入了网页和网页外部JavaScript代码之间的分离上下文,以便每个代码的JavaScript执行不会相互影响。这是消除RCE可能性的必要功能,但这次在Discord中被禁用。
现在我发现contextIsolation被禁用,于是开始寻找可以通过干扰网页外部JavaScript代码来执行任意代码的地方。
通常,在Electron渗透测试中创建RCE的PoC时,我首先尝试使用渲染器上的Electron内部JavaScript代码来实现RCE。这是因为渲染器上的Electron内部JavaScript代码可以在任何Electron应用中执行,所以基本上我可以重用相同的代码来实现RCE,这很容易。
在我的幻灯片中,我介绍了可以通过使用Electron在导航时执行的代码来实现RCE。不仅可以从该代码实现,而且在某些地方存在这样的代码。(我希望将来发布PoC示例。)
但是,根据使用的Electron版本或设置的BrowserWindow选项,由于代码已更改或无法正确到达受影响的代码,有时通过Electron代码的PoC效果不佳。这次它没有奏效,所以我决定将目标改为预加载脚本。
检查预加载脚本时,我发现Discord向网页暴露了一个函数,允许通过DiscordNative.nativeModules.requireModule(‘MODULE-NAME’)调用某些允许的模块。
在这里,我无法直接使用可用于RCE的模块,例如child_process模块,但我发现了一个代码,可以通过覆盖JavaScript内置方法并干扰暴露模块的执行来实现RCE。
以下是PoC。我能够确认,当从devTools调用名为"discord_utils"的模块中定义的getGPUDriverVersions函数时,同时覆盖RegExp.prototype.test和Array.prototype.join,计算器应用程序会弹出。
|
|
getGPUDriverVersions函数尝试使用"execa"库执行程序,如下所示:
|
|
通常execa尝试执行nvidiaSmiPath变量中指定的"nvidia-smi.exe",但是由于覆盖了RegExp.prototype.test和Array.prototype.join,参数在execa的内部处理中被替换为"calc"。
具体来说,通过更改以下两个部分来替换参数:
https://github.com/moxystudio/node-cross-spawn/blob/16feb534e818668594fd530b113a028c0c06bddc/lib/parse.js#L36 https://github.com/moxystudio/node-cross-spawn/blob/16feb534e818668594fd530b113a028c0c06bddc/lib/parse.js#L55
剩下的工作是找到在应用程序上执行JavaScript的方法。如果我能找到它,就会导致实际的RCE。
iframe嵌入中的XSS
如上所述,我发现任意JavaScript执行可能导致RCE,因此我试图找到XSS漏洞。应用程序支持自动链接或Markdown功能,但看起来很好。于是我转而关注iframe嵌入功能。iframe嵌入是当发布YouTube URL时在聊天中自动显示视频播放器的功能。
当发布URL时,Discord尝试获取该URL的OGP信息,如果存在OGP信息,它会在聊天中显示页面标题、描述、缩略图、相关视频等。
Discord从OGP提取视频URL,只有当视频URL是允许的域并且URL实际上具有嵌入页面的URL格式时,该URL才会嵌入到iframe中。
我找不到关于哪些服务可以嵌入iframe的文档,因此我尝试通过检查CSP的frame-src指令来获取提示。当时使用了以下CSP:
|
|
显然,其中一些列出的域允许iframe嵌入(例如YouTube、Twitch、Spotify)。
我尝试通过将域逐个指定到OGP信息中来检查URL是否可以嵌入iframe,并尝试在嵌入的域上查找XSS。经过一些尝试,我发现CSP中列出的域之一sketchfab.com可以嵌入iframe,并在嵌入页面上发现了XSS。
我当时不了解Sketchfab,但它似乎是一个用户可以发布、购买和销售3D模型的平台。在3D模型的脚注中存在一个简单的基于DOM的XSS。
以下是PoC,具有精心构造的OGP。当我将此URL发布到聊天时,Sketchfab被嵌入到聊天中的iframe中,在iframe上点击几次后,执行了任意JavaScript。
https://l0.cm/discord_rce_og.html
|
|
好的,我终于找到了XSS,但JavaScript仍在iframe上执行。由于Electron不会将"网页外部的JavaScript代码"加载到iframe中,因此即使我覆盖了iframe上的JavaScript内置方法,也无法干扰Node.js的关键部分。要实现RCE,我们需要跳出iframe并在顶级浏览上下文中执行JavaScript。这需要从iframe打开新窗口或从iframe将顶部窗口导航到另一个URL。
我检查了相关代码,发现在主进程的代码中找到了使用"new-window"和"will-navigate"事件限制导航的代码:
|
|
我认为这段代码可以正确地防止用户打开新窗口或导航顶部窗口。但是,我注意到了意外的行为。
导航限制绕过(CVE-2020-15174)
我认为代码没问题,但我尝试检查从iframe进行的顶部导航是否被阻止。然后,令人惊讶的是,由于某种原因,导航没有被阻止。我预计尝试会在导航发生之前被"will-navigate"事件捕获,并被preventDefault()拒绝,但事实并非如此。
为了测试这种行为,我创建了一个小的Electron应用。我发现由于某种原因,“will-navigate"事件不会从iframe启动的顶部导航发出。准确地说,如果顶部的源和iframe的源是同源,则会发出事件,但如果它是不同源,则不会发出事件。我不认为这种行为有正当理由,所以我认为这是Electron的错误,并决定稍后向Electron团队报告。
借助这个错误,我能够绕过导航限制。我最后要做的只是使用iframe的XSS导航到包含RCE代码的页面,如top.location=”//l0.cm/discord_calc.html"。
通过这种方式,结合三个漏洞,我能够实现RCE,如下面的视频所示。
最终结果
这些问题通过Discord的漏洞奖励计划进行了报告。首先,Discord团队禁用了Sketchfab嵌入,并通过向iframe添加sandbox属性采取了变通措施以防止从iframe导航。一段时间后,启用了contextIsolation。现在即使我可以在应用程序上执行任意JavaScript,也不会通过被覆盖的JavaScript内置方法发生RCE。我因此发现获得了5,000美元的奖励。
Sketchfab上的XSS通过Sketchfab的漏洞奖励计划进行了报告,并由Sketchfab开发人员快速修复。我因此发现获得了300美元的奖励。
“will-navigate"事件中的错误作为Electron的错误报告给Electron的安全团队,并被修复为以下漏洞(CVE-2020-15174)。
Unpreventable top-level navigation · Advisory · electron/electron https://github.com/electron/electron/security/advisories/GHSA-2q4g-w47c-4674
就是这样。
个人而言,我喜欢外部页面的错误或与应用程序本身实现无关的Electron错误导致RCE的情况:)
希望本文能帮助您保持Electron应用的安全。
感谢阅读!