Tailscale RCE 漏洞複現:CVE-2022-41924
在今年,我架設了我人生中的第一台 NAS 1。完成架設後,一個常見的需求就是能夠在外出時方便地存取 NAS 上的資料或服務,為了解決這個問題,我選擇使用 Tailscale 2。
這篇文章的目的是重現 https://emily.id.au/tailscale 這篇部落格中提到的 CVE-2022-41924 漏洞,該文章沒有提供 POC,所以我會在這篇文章中詳細敘述複現的方式並提供 POC 程式碼。 該漏洞串聯多個小廢洞,最終實現了 RCE。這些洞說複雜沒有到很複雜,說簡單也不簡單,剛好可以讓我們了解整個 Tailscale 的架構和攻擊面。
Tailscale 的架構
Tailscale 的架構由以下三個主要部分組成:
- Tailscale Control Server
- Tailscale Client
- Tailscale Relay Server (DERP)
Tailscale Control Server
Tailscale Control Server 或叫做 Coordination Server(以下簡稱為 Control Server)負責管理 WireGuard 憑證交換以及 ACL 權限控制。
官方提供的 Control Server 是 login.tailscale.com
,而開源版的叫做 Headscale。
Tailscale Client 通過 long-polling 定期從 Control Server 拉取 ACL 配置,用於限制 inbound traffic 是否被允許。 題外話的部分,這裡有個小 trick 是如果你在某台 Tailscale Client 上,但是其他人沒辦法連線到你,可以直接改自己的 Client 的原始碼,把 ACL 無效,就可以正常連線,忽略 Control Server 設定的權限,也就是說其實底層的 WireGuard 連線是通的,只是 Client 擋住 inbound traffic 而已。3
Tailscale Client
Tailscale Client 之間會透過 WireGuard 連線並組成 Mesh Network,使所有設備互相直連,而非依賴中心化的伺服器。 它提供以下功能,就像是一個 超級後門。
- SSH: 內建 SSH Server
- Taildrop: 可以接受別的節點傳來的檔案
- Taildrive: 內建 WebDAV Server
- Funnel: 暴露內網的服務到外網
可不可以對其它節點 SSH, Taildrop 傳檔案,這是設定在 Control Server 上的,所以可以看得出 Control Server 的權力很大,就是他們的老大哥。
Tailscale 的內網被稱作 Tailnet,而 Tailnet 的內網 IP 範圍是 100.64.0.0/10
(CGNet4),這個範圍實際上是屬於 Public IP 的部分,這與我們之後要 bypass SOP 的技巧有關。
另外在 Tailnet 的 IP 中有一個特別的存在,就是 100.100.100.100
(Quad100)5,這相當於 Tailnet 中的 127.0.0.1 位址,我們可以透過訪問 http://100.100.100.100
的網頁得知我們自己的內網的 IP。
Tailscale Relay Server
這是 NAT 打洞失敗的備案,充當中繼伺服器,類似 TURN Server。本篇文章將不討論這部分的細節。
CVE-2022-41924 漏洞分析
這個漏洞串聯多個小廢洞,最終實現了 RCE。
攻擊情境如下:
- 受害者在 Windows 上安裝了 Tailscale Client。
- 攻擊者通過特製網頁觸發漏洞,最終在受害者桌面上創建檔案並執行任意程式。
基本上我們是從網頁直接打到 Windows RCE,十分刺激🥳
有漏洞的版本是 Tailscale Windows v1.32.2
版。
另外我們將使用 Headscale v0.17.0-beta2
來複現漏洞。
在進一步講解漏洞之前,我先科普一下 Tailscale Client 中的兩個重要 API,分別是 Local API 和 Peer API。
Local API
Tailscale Client 包含前端 GUI / CLI 和後端 tailscaled
兩個部分。
在 Windows 上,前端通過 HTTP Protocol 與後端的 Local API 互動 (綁定在 localhost:41112
),而不是 Unix Socket。這樣的設計給了攻擊者機會透過網頁直接戳到 Local API。
Local API 提供多種功能,這裡重點介紹兩個與漏洞相關的功能:
/localapi/v0/prefs
- 管理 Tailscale Client 的設定。攻擊者可以發送 PATCH 請求更改
ControlURL
,將 Client 的 Control Server 指向自己控制的伺服器,進而完全掌控該 Client。
- 管理 Tailscale Client 的設定。攻擊者可以發送 PATCH 請求更改
/localapi/v0/files
- 列出 Taildrop 收到的檔案,功能類似
python -m http.server
生成的索引頁面,並允許讀取或刪除檔案。
- 列出 Taildrop 收到的檔案,功能類似
Peer API
Peer API 同樣是一個 HTTP 服務,但與 Local API 不同,它綁定在 Tailscale 為每個節點分配的 Tailnet IP(例如 100.64.0.1
),而非本地的 localhost
。Peer API 開放的 Port 是基於 IP 的 CRC32 值計算得出。
Peer API 的功能之一是 Taildrop。用於在節點之間傳送和接收檔案,類似 SSH 的 scp
命令。
要使用 Peer API 傳送檔案給另一個節點,我們可以發送 PUT 請求到 http://100.64.0.1:58436/v0/put/test.txt
並附帶檔案內容。
觸發 RCE
科普結束! 我們馬上來看看這個 RCE 要怎麼觸發。首先,我們必須先發送 PATCH 請求到 http://localhost:41112/localapi/v0/prefs
將 Control Server 指向我們控制的伺服器。
diff --git a/api_common.go b/api_common.go
index b4983cc..a8ce571 100644
--- a/api_common.go
+++ b/api_common.go
@@ -64,6 +64,7 @@ func (h *Headscale) generateMapResponse(
PacketFilter: h.aclRules,
DERPMap: h.DERPMap,
UserProfiles: profiles,
+ PopBrowserURL: "file:///C:/Windows/System32/calc.exe",
Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort,
控制 Control Server 之後,Client 會定期向我們報到,我們就可以在回傳 Client 的訊息中多加一個 PopBrowserURL
的欄位,這個欄位是用來讓 Client 跳轉到一個網頁,而在 Windows 中,我們可以跳轉到 file:///C:/Windows/System32/calc.exe
,觸發程式執行,而要執行我們想要執行的程式,我們只要先透過 Peer API 把程式傳送進去,就可以成功執行,達到 RCE。
感覺挺簡單的,但這裡我們還有個大問題要解決,就是要如何繞過 Same Origin Policy 保護,戳到 Local API 和 Peer API。
複習 Same Origin Policy (SOP)
這邊簡單複習一下 SOP 的概念,SOP 是瀏覽器的安全機制,用來限制不同來源之間的互動。
那怎麼樣算是同源,這裡有三個條件:
- Protocol:例如,
http
和https
是不同的來源。 - Host:例如,
example.com
和sub.example.com
是不同的來源。 - Port:例如,
example.com:80
和example.com:8080
是不同的來源。
不同源會有什麼限制,主要分為三種情境:
- Cross-origin writes are typically ✅ allowed
- Cross-origin reads are typically ❌ disallowed
- Cross-origin embedding is typically ✅ allowed
我翻譯一下,在跨源的情況下,比如在 https://hacker.com
的網頁中發送一個 GET
請求到 https://example.com
的網頁,雖然送得出去 (writes allowed),但回傳的東西是看不到的 (reads disallowed),會被瀏覽器擋住。
而在 https://hacker.com
的網頁中置入一個 https://example.com
的 iframe 也是允許的 (embedding allowed),但是也讀不到內容 (reads disallowed),兩個是獨立運行的,等同於開了個新分頁。
關於 preflight, CORS, CSRF 等概念,我們之後再專門做一篇文章...
繞過 Same Origin Policy (SOP)
那要繞過 SOP 的限制,其中一個方法就是 DNS Rebinding 的手法。
在現今的環境下,因為一個新的規範 Private Network Access (CORS-RFC1918) 的出現6,限制了 DNS Rebinding 無法直接從 Public IP 進行 Rebind 到 Private IP。所幸 Tailnet 內網使用的是 CGNet (屬於 Public IP),所以還是可以戳到 Peer API,但要戳到 Local API 還需要利用 Peer API 的一個特性。
還記得我們可以發送 PUT 請求到 http://100.64.0.1:58436/v0/put/test.txt
這件事,我們細說一下底層的邏輯。在有人透過 Taildrop 送檔案過來之後,這時檔案會被 tailscaled
暫存到 C:\ProgramData\Tailscale\files\<id>
資料夾內,接下來,GUI Client 會自動打一個 GET 請求到 http://localhost:41112/localapi/v0/files
Local API 去讀取這個檔案,讀完之後把檔案複製到桌面,最後送一個 DELETE 請求把檔案刪掉。
這裡有兩個問題,第一個問題是這個取出來和刪掉的動作有 Race Condition 的問題,同時傳送多個同樣檔名的檔案,就有機會檔案沒有被刪掉,還殘留在暫存資料夾內,這樣就可以從 Local API 中訪問到。
第二個問題是 Local API 回傳的 Content-Type
是 text/html
,這樣我們可以把一個 html 的檔案送進去,就可以在與 Local API 同源的 localhost
網頁底下執行任意的 javascript。
RCE 攻擊步驟
最後我們來看實際的攻擊步驟。
- 第一次 DNS Rebinding (
hacker.com
-> Quad100)- 先戳 Quad100 的網頁去取得內網 IP,假設是
100.64.0.1
。
- 先戳 Quad100 的網頁去取得內網 IP,假設是
- 開 iframe 進行第二次 DNS Rebinding (
hacker.com
->100.64.0.1
)- 戳 Peer API 把兩個檔案傳進去
- 我們要執行的程式
- 下一階段要戳 Local API 的 html,假設是
hack.html
- 戳 Peer API 把兩個檔案傳進去
- 再開 iframe (
http://localhost:41112/localapi/v0/files/hack.html
)- 戳 Local API 把 Control Server 指向我們控制的伺服器
最後透過 PopBrowserURL
觸發 RCE 🎉
我是用 singularity 這個工具做 DNS Rebinding,並不是每次都會成功,需要多試幾次。我在實作的時候發現 Edge 瀏覽器無法做 DNS Rebinding,但是 Firefox 可以,應該是 Edge 還做了什麼保護。
我這篇文章專注在複現 RCE,稍微省略了一些細節,建議也可以去閱讀一下漏洞原作者的文章:https://emily.id.au/tailscale