HackMD XSS, Again
2023-6-22 16:14:23 Author: govuln.com(查看原文) 阅读量:34 收藏

不過這次最特別的是我在 HackMD 團隊修復後還是有找到方法 bypass 它的 filter,在一樣的地方再拿一個 XSS。後來回顧時還發現到了一個先前沒找到的 XSS,所以這篇文章一共有三個 HackMD XSS。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
a.fn.gist = function(c) {
return this.each(function() {
var e, f, g, h, i, j, k, l, m, n, d = a(this), o = {};
return d.css("display", "block"),
e = d.data("gist-id") || "",
g = d.data("gist-file"),
k = d.data("gist-hide-footer") === !0,
l = d.data("gist-hide-line-numbers") === !0,
h = d.data("gist-line"),
j = d.data("gist-highlight-line"),
n = d.data("gist-show-spinner") === !0,
m = n ? !1 : void 0 !== d.data("gist-show-loading") ? d.data("gist-show-loading") : !0,
g && (o.file = g),
e ? (f = "https://gist.github.com/" + e + ".json",
i = "Loading gist " + f + (o.file ? ", file: " + o.file : "") + "...",
m && d.html(i),
n && d.html('<img style="display:block;margin-left:auto;margin-right:auto" alt="' + i + '" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif">'),
void a.ajax({
url: f,
data: o,
dataType: "jsonp",
timeout: 2e4,
success: function(c) {
var e, g, i, m, n;
c && c.div ? (c.stylesheet && (0 === c.stylesheet.indexOf("<link") ? c.stylesheet = c.stylesheet.replace(/\\/g, "").match(/href=\"([^\s]*)\"/)[1] : 0 !== c.stylesheet.indexOf("http") && (0 !== c.stylesheet.indexOf("/") && (c.stylesheet = "/" + c.stylesheet),
c.stylesheet = "https://gist.github.com" + c.stylesheet)),
c.stylesheet && 0 === a('link[href="' + c.stylesheet + '"]').length && (e = document.createElement("link"),
g = document.getElementsByTagName("head")[0],
e.type = "text/css",
e.rel = "stylesheet",
e.href = c.stylesheet,
g.insertBefore(e, g.firstChild)),
n = a(c.div),
n.removeAttr("id"),
d.html("").append(n),
j && (m = b(j),
n.find("td.line-data").css({
width: "100%"
}),
n.find(".js-file-line").each(function(b) {
-1 !== a.inArray(b + 1, m) && a(this).css({
"background-color": "rgb(255, 255, 204)"
})
})),
h && (i = b(h),
n.find(".js-file-line").each(function(b) {
-1 === a.inArray(b + 1, i) && a(this).parent().remove()
})),
k && (n.find(".gist-meta").remove(),
n.find(".gist-data").css("border-bottom", "0px"),
n.find(".gist-file").css("border-bottom", "1px solid #ddd")),
l && n.find(".js-line-number").remove()) : d.html("Failed loading gist " + f)
},
error: function(a, b) {
d.html("Failed loading gist " + f + ": " + b)
},
complete: function() {
"function" == typeof c && c()
}
})) : !1
})
}

顯然,裡面有各種字串拼接 html 的操作,所以應該可以有 XSS。然而 data-gist-id 在前面會被 stripTags 過濾,所以要看看它是怎麼做的:

這邊 t 部分因為呼叫時沒有給參數,所以會是預設的那個 [""],因此它相當於 e.replace(/<\/?[^<>]*>/gi, ""),所以 <<x>script> 就能繞過了,之後就想辦法讓它觸發 error callback 的 d.html("Failed loading gist " + f + ": " + b) 就行了。

基本上是 replace 一樣的東西,但是這次還會 recursive 的把 html tag 移除掉,所以 <<x>script> 這種繞法是沒用的。不過由於它有 match >,所以 <script 這種沒閉合的 tag 是不會被移除的。

在不能用 > 的情況下從 cheat sheet 上查可知有 <svg onload=alert()// 之類的 payload,不過這邊一個是因為 CSP 所以不能用,另一個是它 html 注入的地方是使用 jQuery $.fn.html 去做的,裡面會 assign html 給 innerHTML。而瀏覽器在 assign html 給 innerHTML 時我發現如果只有 < 存在而沒有 > 的話,< 之後的所有 payload 都會整個消失不見,所以這樣是沒辦法的。

前面可以很簡單的把 img 結束掉,後面又有 > 的存在,所以只有 < 的話可以用 iframe srcdoc 去搞事。不過這邊有個困難點是 srcdoc 裡面還是要 bypass CSP,因此它一樣要用 Google jsonp CSP bypass,但同時它又有個呼叫 Gist 的 ajax,無論是 success 還是 error 都會呼叫 d.html(...),iframe 還在載入中的 Google jsonp 覆蓋過去,所以沒辦法 XSS。

這邊我的想法是能不能讓它憑空生一個 error,讓它不會執行到 d.html(...),這樣才有足夠的時間能載入 jsonp。而這邊我的答案就是在 Gist callback 利用我之前在 HITCON CTF 2022 - Secure Paste 用的那個 jsonp delete 技巧 (實際上真正的來源應該是這個),用 delete[jQuery.fn][0].html 就能把 $.fn.html 刪掉,這樣就能讓它產生 error 了,所以就有足夠的時間讓 Google jsonp 載入得到 XSS。

最後有個我原本沒注意到的點,就是 setTimeout(window.viewAjaxCallback, 200) 這個地方。原本 window.viewAjaxCallback 是有個函數存在的,但我們從上面知道可以用 jsonp callback 去 delete 某個東西,所以用 delete[window][0].viewAjaxCallback 就能把它刪掉。

接下來因為 setTimeout 的第一個參數其實也能接受 string,所以有機會用 DOM clobbering 去蓋掉 window.viewAjaxCallback 的值達成 XSS:


文章来源: https://govuln.com/news/url/YDky
如有侵权请联系:admin#unsafe.sh