作者:0xcc
原文鏈接:https://mp.weixin.qq.com/s/vfPxiLqOVZWhFde_2fKf1Q

圖片

Dash App 是 macOS 上一款非常流行的查看離線 API 文檔的應用,由個人開發者@kapeli 發布。支持離線文檔查詢和多種 IDE 的集成,對軟件開發者是一款極為實用的生產力工具。在相當多數的 macOS 使用攻略上都能看到這款軟件的推薦。

而在 2018 年我向 Dash 的開發者報告了一些安全問題,并將其設計為第一屆 RealWorld CTF 預選賽的題目。出人意料的是,來自各國的 CTF 選手在很短時間內找到了更為嚴重的遠程代碼執行漏洞。在賽后和開發者郵件溝通后,Dash 很快推出了補丁之后的版本。

在 2019 年 12 月 3 日的 Dash 5 更新(https://blog.kapeli.com/dash-5)之前,Dash 一直用的是舊的 WebView API。后來因為 WebView 被標記為過時,升級到了 WKWebView 控件。

而下面提到的安全問題,無一不與 WebView 遺留 API 的設計有關。

本地任意文件讀取

Dash 在展示文檔內容的時候主要使用 HTML 格式。在 mac 上,許多程序使用 bundle(包)來組織可執行代碼和文件內容。Dash 的 bundle 后綴名為*.docset,包含如下內容:

  • Contents/Resources/Documents/ HTML 文檔和資源
  • Info.plist 包的元數據(meta data),如標識符等
  • Contents/Resources/docSet.dsidx 基于 SQLite 數據庫的索引,用以加快詞條檢索

Dash 從網上下載 docset 之后,將保存在本地。因此 WebView 當中實際出現的 URL 就是 file:/// 域下的。

WebView(對應 iOS 上的 UIWebView)載入 file 域會直接導致 UXSS。WebView 默認允許了 allowFileAccessFromFileURLs 和 allowUniversalAccessFromFileURLs,所以通過 AJAX 可以以絕對路徑讀取任意本地文件的內容,并發送到遠程服務器。

xhr = new XMLHttpRequest();
xhr.onopen = function() {
  alert(xhr.responseText);
}
xhr.open('GET', 'file:///etc/passwd', true);
xhr.send();

不過在 2018 年初的 Dash 版本這招不起作用。在 WebView(和 UIWebView)中可以通過實現 NSURLProtocol 的子類來攔截特定 URL scheme 的網絡請求,實現自定義的資源加載邏輯。這個類對于有 iOS 應用開發的讀者來說不會陌生。

Dash 當時主要用 NSURLProtocol 實現了兩種場景:

  1. 處理本地 HTML 引用的 javascript 和圖片等資源,修復相對路徑加載的問題
  2. 用來實現特殊頁面的跳轉,例如后面會提到的 dash-man-page://,在網頁嘗試加載此類自定義 URL 的時候觸發一些設計好的功能

當時的 Dash 就意識到了 file:// 域文件可以任意讀取的問題,便限制了只能訪問 docset 包內的路徑。

不過我找了一個簡單的繞過。docset 本質上是一個文件夾,因此從網上下載的都是壓縮好的 tar.gz 格式——而壓縮包支持符號鏈接。因此只需要創建一個根目錄 “/” 的符號鏈接,即可重操舊業——偷文件。

因此攻擊場景就是,在存在漏洞的 Dash 上下載導入了一個惡意的 docset,瀏覽這個文檔可能導致計算機上任意文件(如 SSH 私鑰)被竊取。

遠程任意文件讀取

Dash 在閱讀文檔時還有一個分享功能,會隨機選擇一個端口開啟 HTTP 服務,網址類似:http://127.0.0.1:60815/Dash/hpzzlcsf/nodejs/api/os.html

前文提到的符號鏈接問題在這里同樣奏效,不過這個服務是基于 GCDWebServer 實現的,所以還有一個更顯而易見的路徑穿越漏洞。在請求的路徑中使用 ..%2F 可以被解碼為 ../ 字符,從而遠程讀取任意路徑文件。

這種攻擊不需要特殊的 docset 包,只要局域網內掃描到這個服務器端口即可。

訪問本地 electron 調試端口

前面提到的 UIWebView UXSS 問題除了能讀文件之外,還有一個不起眼但是危害不可小覷的任意 http 請求問題。不過這個問題需要有其他軟件的協同,這也是 RealWorldCTF 最開始的出題思路。

近年來 VSCode 為代表的編輯器基于 node.js 和 Electron(或 CEF)技術,使用 HTML 開發界面,極大方便了擴展的生態和功能的迭代。雖然運行資源吃得不少,但是帶來的體驗還是讓許多用戶大呼真香。

在 VSCode 的歷史版本(1.19.0~1.19.2)當中存在一個遠程代碼執行漏洞。這一系列版本的 VSCode 錯誤地在生產環境打開了 Electron 的遠程調試端口,任何能發起跨域 http 請求的網頁,都可以通過訪問如下 URL 獲得一個 token:http://127.0.0.1:9333/json/list

[ {
 "description": "node.js instance",
 "devtoolsFrontendUrl": "chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9333/c5408ce2-6f06-4a7e-a950-395d95c6804f",
 "faviconUrl": "<https://nodejs.org/static/favicon.ico>",
 "id": "c5408ce2-6f06-4a7e-a950-395d95c6804f",
 "title": "/private/var/folders/4d/1_vz_55x0mn_w1cyjwr9w42c0000gn/T/AppTranslocation/EE69BB42-2A16-45F3-BB98-F6639CB594B1/d/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper",
 "type": "node",
 "url": "file://",
 "webSocketDebuggerUrl": "ws://127.0.0.1:9333/c5408ce2-6f06-4a7e-a950-395d95c6804f"
} ]

其中的 webSocketDebuggerUrl 可以直接建立一個 WebSocket 連接,接著使用 Chrome 遠程調試協議(基于 JSON 和 WebSocket)即可向 node.js 解釋其注入任意 js 代碼,從而控制用戶的計算機。

在受影響的版本當中,這個 9333 端口存在一個 DNS rebinding 問題,可以通過短時間內切換 DNS 解析的結果來繞過瀏覽器同源策略獲得 localhost 的內容,接著 WebSocket 默認不限制跨域訪問,導致任何瀏覽器只要訪問了攻擊者的網站停留大約兩分鐘即可被入侵。

VSCode 的修復方案是增加了針對 dns rebinding 的校驗,并在后續版本中隨機化調試端口。在寫這篇文章的時候,所有的調試端口已經不再開啟。類似地,另一款來自 Adobe 的編輯器 Brackets 也采用了 CEF 和 HTML 的方案。

雖然兩個編輯器都修復了 DNS rebinding 問題,導致這個端口的響應內容無法跨域獲取,不過回到 Dash 的 WebView 上,我們前面已經說了這個 UXSS 是沒有同源策略限制的。

假如讓 Dash 和(存在漏洞的)Brackets 或者 VSCode 同時運行,在 Dash 當中打開的惡意文檔,就可以直接通過向調試端口發起請求注入 js 代碼的方式執行任意本地代碼。

217 戰隊使用預期解法在 RealWorldCTF 解出了這個題目:

https://blog.l4ys.tw/2018/07/realworld-ctf-2018-doc2own/

多個命令注入和命令執行

然而在比賽期間我們收到了非預期的 0day 解法。為了讓用戶有時間升級,這些 writeup 從未公開過。

PPP 戰隊和 CyKOR 使用了同一個命令注入問題。

我們前文提到,Dash 通過 NSURLProtocol 處理一些預定義的 URL 請求,其中有一個 dash-man-page://,會打開一個終端窗口并運行 man 命令。

圖片”結尾,并包含一對完整的括號時,Dash 會從網址中提取字符串并拼接到 bash 命令。因此可以直接命令注入:

dash-man-page://load?query=open -a Calculator(1)

圖片

此外如果 docset 當中存在一個名為 cat2html 的 shell 腳本,也會執行。

Eat, Sleep, Pwn, Repeat 使用了另一個(不夠完美)的命令執行問題。

在 WebView(UIWebView)里提供了一個 Api,可以直接在網頁的 JavaScriptCore 運行時(參考 JSContext 類)當中提供額外的函數和對象:

之前的 Dash 應用在 js 里注入了一個 window.dash 對象,可以訪問 DHWebViewController 上的方法。

通過 JSContext 注入的 Objective-C 方法有命名轉換規則。如果對象定義了 webScriptNameForSelector: 方法,則優先使用該方法中自定義的名字;默認情況下,方法名(Objective-C selector)當中的冒號會轉義為下劃線(_),而下劃線則使用 替換為連續兩個美元符號。

例如 Objective-C 當中的方法是 setFlag:,在 js 里調用時寫作 obj.setFlag_(flag)。

另外對象可以定義一個 isSelectorExcludedFromWebScript: 方法來控制 js 能使用的 selector 列表,相當于一個白名單。在 Dash 里這個方法實現如下:

char __cdecl +[DHWebViewController isSelectorExcludedFromWebScript:](id a1, SEL a2, SEL a3)
{
  return "coffeeScriptOpenLink:" != a3
      && "showFallbackExplanation" != a3
      && "openDownloads" != a3
      && "openGuide" != a3
      && "jsGoToURL:" != a3
      && "openDocsets" != a3
      && "openProfiles" != a3
      && "openGift" != a3
      && "loadFallbackURL:" != a3
      && "setUpTOC" != a3
      && "version" != a3
      && "unityConsoleLog:" != a3
      && "msdnMakeActive:" != a3
      && "openExternal:" != a3
      && "switchAppleLanguage:" != a3
      && "toggleAppleOverview:" != a3
      && "openIOSLink" != a3
      && "openPawLink" != a3
      && "closeAnnounce" != a3
      && "useSnippet" != a3;
}

其中的 openExternal: 方法從 js 接收一個字符串參數,轉為 URL 之后直接用系統默認的關聯協議打開:

圖片

當我們傳入一個可執行文件的 bundle 的 file:/// URL 時,相當于在 Finder 里雙擊執行 app,也就是一個代碼執行向量。ESPR 就在 docset 里嵌入了一個可執行的 .app,然后通過 js 運行:

var url = location.href.toString()
    .replace('some.html', 'some.app') // file:///path/to/.app
window.dash.openExternal_(url)

圖片

這個方式有一個局限性。如果 docset 是從網上下載回來的,解壓之后的文件會被標記 com.apple.quarantine 屬性。雙擊運行其中的 app 會觸發 GateKeeper,有可信的數字簽名會提示用戶是否繼續運行,簽名無效則會提示文件已損壞。

因為比賽的運維系統使用了其他上傳方式,就沒有受到 GateKeeper 影響。

在賽后我很快聯系了作者,并找到了更多的攻擊向量:

  • 處理文檔打印存在一處命令注入
  • 處理蘋果的文檔時,如果 bundle 內存在 Apple Docs Helper/Apple Docs Helper 可執行文件,會嘗試運行

作者修復了命令注入,并在運行可執行文件前檢查代碼的數字簽名,也檢查了線上倉庫的文檔以確保此前沒有被惡意上傳過。

結語

針對開發者直接投毒的攻擊近幾年時有發生,類似 Dash 這樣流行的生產力工具也不失為一個可能的方式。通過舉辦一場 CTF 的方式竟然在極短時間內找到了存在實際危害的數個 0day 漏洞,確實硬核。

這篇文章里提到的一些具體案例和 WebView 這個遺留 API 的設計存在很大關系,蘋果在 iOS 12(2018 年最新的系統是 10)之后明確標記 UIWebView 為過時 API,還在應用商店審核規則中加強限制對老舊 API 的使用,也是為了提升安全性和性能。對開發者來說升級控件并不是一個簡單的查找替換過程,不過為了用戶考慮,還是得做一些犧牲。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1548/