Codegate 2023 部分Write-Up (Web/Misc)
这次去韩国参加了Codegate 2023的决赛!把Web部分的题解在此释出,其他游记/照片另开一文。
Warmup
没咋看。似乎是XSS绕绕。
Pwn
是的,这题就叫Pwn(与之相对的是Pwn分类里有道题叫Web)
解法:没咋看。应该是比较水的一题。经典数组绕过
1 | data = "username[0]=\\' union select password from USER WHERE username='admin' -- &username[1]=2&password=3" |
Cha’s magic
源码:
1 | <?php |
也就是v1和v2和v3经过hash得到的值都一样才能拿到完整flag。
对于OLD_PASSWORD的算法,网上有很多实现,这里有一个C#版本的:
1 | // Online C# Editor for free |
可以看到,v1和v2的hash值碰撞只需要加入空格就可以:因为该算法完全忽略空格和制表符。
这样前一半flag就知道了:codegate2023{Mysql_olD_P455woRD_I5_CoMP
核心问题在于使v2的hash值等于6368616368613230
,v2必须以i_love_you}
结尾,我们唯一能改变的就是中间的字符串。
经过搜寻,找到一个帖子:mysql-old-password-cryptanalysis
里面给出了一段逆向推导nr与nr2的代码,以及一篇分析该算法的论文:F. Muller and T. Peyrin “Cryptanalysis of T-Function-Based Hash Functions” in International Conference on Information Security and Cryptology - ICISC 2006
不过需要注意的是该论文中的算法和实际算法有一个微小差异:nr2对应公式的=
应为+=
。
逆向计算nr/nr2:
1 |
|
也就是,在已知add值(字符串的前缀ASCII和)以及当前字符的情况下,对于平凡情况(无多解),能在常数时间确定上一个nr/nr2对的值。
一个朴素的思路是暴力枚举当前位的字符与add值。
暴力计算的话时间复杂度肯定是无法接受的(O(64^n),5步需要约1分钟)。我们理一下现在已知什么,需要求解什么。请注意:我们需要的只是hash碰撞,不代表flag也是以i_love_you}
结尾的。
- v2的形式:
codegate2023{Mysql_olD_P455woRD_I5_CoMP<???>i_love_you}
- FLAG的总长度为78-79(因为前一半flag长为39)
- 最终hash(v2)=
6368616368613230
-> 最终的nr/nr2 - 前一半flag的add值为3430:
sum([ord(i) for i in "codegate2023{Mysql_olD_P455woRD_I5_CoMP"])
- 结尾的11个字符
i_love_you}
对应add值:1207 - 未知:整个flag字符串的add值
- 但add值的可能范围很有限,我们可以容易计算出上下界:下界为3430+39*32=4678,上界为3430+40*127=8510
- 并且,这个范围内合法的add值占比并不高:对于一个可能的add值,往回推结尾的那11个字符
i_love_you}
必须都有解
所以我们只需要MitM,中间相遇,即可爆出正解。
这个中间状态指的是<???>
这串未知字串中间某个位置一个独特的nr/nr2/add对;而这个add值毕竟只是一个中间量(下称fadd),可以手动给予一个值。在本题中我们手动要求从codegate2023{Mysql_olD_P455woRD_I5_CoMP<???>i_love_you}
再向前填充总计6*127
的字符作为中间相遇处:当然越大的值搜索空间越大。
- 我们先筛选出可行的add值,并记录在处理
i_love_you}
前的中间add值 - 逆向: 对于每个中间add值先逆推5步,记录所有可能在中间相遇处add值为fadd的nr/nr2对,以及逆向对应的5个字符
- 正向: 我们手动要求向前数步(至少6步,因为fadd值是3430+6*127)到达中间相遇处使得add值为fadd,并检查这个nr/nr2对是否出现在逆向得到的结果中;如果有,则拿下。
爆破代码:Credit to @4qwerty
为什么for嵌套这么多?因为4老师懒得写递归,反正没几步,不够就加一层for就行了。
其实如果要计算的话是可以的:逆着5步大约有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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
static int
oldpw_rev(uint32_t *pnr, uint32_t *pnr2, uint32_t add,
unsigned char *cc, unsigned len)
{
uint32_t nr, nr2;
uint32_t c, u, e, y;
if (len == 0) {
return 0;
}
nr = *pnr;
nr2 = *pnr2;
c = cc[len - 1];
add -= c;
u = nr2 - nr;
u = nr2 - ((u << 8) ^ nr);
u = nr2 - ((u << 8) ^ nr);
nr2 = nr2 - ((u << 8) ^ nr);
nr2 &= 0x7FFFFFFF;
y = nr;
for (e = 0; e < 64; e ++) {
uint32_t z, g;
z = (e + add) * c;
g = (e ^ z) & 0x3F;
if (g == (y & 0x3F)) {
uint32_t x;
x = e;
x = y ^ (z + (x << 8));
x = y ^ (z + (x << 8));
x = y ^ (z + (x << 8));
nr = y ^ (z + (x << 8));
nr &= 0x7FFFFFFF;
if (oldpw_rev(&nr, &nr2, add, cc, len - 1) == 0) {
*pnr = nr;
*pnr2 = nr2;
return 0;
}
}
}
return -1;
}
void forward(unsigned char *cc, unsigned len, uint32_t *nr, uint32_t *nr2, uint32_t *add) {
uint32_t tmp;
int i;
for (i = 0; i < len; i++) {
if (cc[i] == ' ' || cc[i] == '\t')
continue;
tmp = cc[i];
*nr ^= (((*nr & 63) + *add) * tmp) + (*nr << 8);
*nr2 += (*nr2 << 8 ) ^ *nr;
*add += tmp;
}
uint32_t mask = (((uint32_t)1 << 31) - (uint32_t)1); // 111....11, 31bits
*nr &= mask;
*nr2 &= mask;
// printf("hash: %x %x\n", *nr, *nr2);
}
void calc(unsigned char *cc, unsigned len, uint32_t *nr, uint32_t *nr2) {
*nr = 1345345333;
*nr2 = 0x12345671;
uint32_t add = 7;
forward(cc, len, nr, nr2, &add);
}
struct Ans {
unsigned char buf[6];
};
struct pair_hash
{
template <class T1, class T2>
std::size_t operator() (const std::pair<T1, T2> &pair) const {
return std::hash<T1>()(pair.first) ^ std::hash<T2>()(pair.second);
}
};
bool ok_maps[65536] = {0};
int main() {
uint32_t nr, nr2;
unsigned char cp[] = "codegate2023{Mysql_olD_P455woRD_I5_CoMP_bEEE_KE03";
nr = 1345345333;
nr2 = 0x12345671;
uint32_t add = 7;
forward(cp, strlen((char *)cp), &nr, &nr2, &add);
printf("nr: %x, nr2: %x, add: %x\n", nr, nr2, add);
uint32_t fnr = nr, fnr2 = nr2, fadd = add;
uint32_t uadd = 127 * 6, fuadd = fadd;
fadd += uadd;
unsigned char cs[] = "i_love_you}";
std::vector<std::thread> ths;
std::mutex mtx;
std::unordered_map<std::pair<uint32_t, uint32_t>, Ans, pair_hash> maps;
std::atomic<int> cnter{0};
for (int i = 500; i < 20000; i++) { // find possible `add` value
uint32_t pnr = 0x63686163, pnr2 = 0x68613230;
int ans = oldpw_rev(&pnr, &pnr2, i, cs, 11);
if (ans != 0) continue;
ok_maps[i] = true;
}
for (size_t i = 33; i < 127; i++) {
ths.emplace_back([&, i] {
std::vector<std::pair<std::pair<uint32_t, uint32_t>, Ans>> all;
Ans cc;
cc.buf[5] = 0;
cc.buf[4] = i;
for (size_t c = 33; c < 127; c++) {
cc.buf[3] = c;
for (size_t d = 33; d < 127; d++) {
cc.buf[2] = d;
for (size_t a = 33; a < 127; a++) {
int sumer = fadd + i + c + d + a + 1207;
cc.buf[1] = a;
for (size_t b = 33; b < 127; b++) if (ok_maps[sumer + b]) {
cc.buf[0] = b;
uint32_t pnr = 0x63686163, pnr2 = 0x68613230;
int ans = oldpw_rev(&pnr, &pnr2, fadd + i + c + d + a + b + 1207, cs, 11); // backward: suffix
if (ans != 0) continue;
ans = oldpw_rev(&pnr, &pnr2, fadd + i + c + d + a + b, cc.buf, 5); // ... and extra 5 steps
if (ans != 0) continue;
++cnter;
all.emplace_back(std::make_pair(std::make_pair(pnr, pnr2), cc)); // possible nr/nr2 pair, record it
}
}
}
}
std::lock_guard<std::mutex> lock(mtx);
for (auto &&p:all) {
maps[p.first] = p.second;
}
});
}
for (auto &&th:ths) th.join();
ths.clear();
puts("MITM");
printf("%d\n", cnter.load());
for (size_t i = 33; i < 127; i++) { // forward
ths.emplace_back([&, i] {
unsigned char buf[50];
buf[0] = i;
for (size_t c = 33; c < 127; c++) {
buf[1] = c;
for (size_t d = 33; d < 127; d++) {
buf[2] = d;
for (size_t a = 33; a < 127; a++) {
buf[3] = a;
for (size_t b = 33; b < 127; b++) {
buf[4] = b;
for (size_t e = 33; e < 127; e++) {
buf[5] = e;
int re = uadd - i - c - d - a - b - e;
int iter = 6;
while (re >= 33) { // padding, to make the `add` value is `fadd`
int tw = re;
if (re > 126) {
tw = 'a';
}
buf[iter++] = tw;
re -= tw;
}
if (re != 0) continue;
buf[iter] = 0;
uint32_t nr = fnr, nr2 = fnr2, add = fuadd;
forward(buf, iter, &nr, &nr2, &add);
auto cc2 = std::make_pair(nr, nr2);
if (maps.find(cc2) != maps.end()) { // GOT!!!
printf("%s%s%s%s\n", cp, buf, maps[cc2].buf, cs);
// exit(0);
}
}
}
}
}
}
});
}
for (auto &&th:ths) th.join();
return 0;
}
Cha’s Diary
非预期了。
储存型XSS,允许跨域。
有CSP策略保护,iframe被禁止加载,需要nonce来执行脚本:<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-<%= nonce %>'; frame-src 'none'; object-src 'none'; style-src 'unsafe-inline' https://unpkg.com">
目标是偷cookie里的flag.
我们可以利用meta标签进行跳转:<meta http-equiv="refresh" content="1; http://attacker.com">
那么我们接下来要做的就是两步:获取nonce—>执行js
获取nonce
正解应该是通过注入CSS,用CSS匹配器匹配nonce属性,逐步获取nonce。
可能有人会好奇,每次页面刷新不是会重新生成nonce吗?但此题有一个特殊之处:传参方法是通过URL的#
锚(哈希/frame/…)来传参,并通过页内js动态渲染。因此当前进后退时并不会刷新,而是调用浏览器缓存。因此nonce不会改变。
当时以为有nonce hiding这种东西,所以没有往下尝试,结果赛后发现这似乎是个饼并未实现……当然,我们的做法是直接在新标签页fetch并拿到页面内容正则匹配,根本就不经过浏览器过滤。
官方的做法就是使用CSS注入每次偷一位,具体做法见how-to-bypass-csp-nonces-with-dom-xss
核心部分:
1 | The summary of this attack is that it's possible to create a CSS program that exfiltrates the values of HTML attributes character-by-character, simply by generating HTTP requests every time a CSS selector matches, and repeating consecutively. If you haven't seen this working, take a look here. The way it works is very simple, it just creates a CSS attribute selector of the form: |
然而我们发现他只使用了Math.random
来生成随机nonce,这完全是可以预测的:https://github.com/PwnFunction/v8-randomness-predictor
于是我们新开一个标签页然后多次fetch阅读文章页面拿一堆nonce给预测器,直接就能知道下一个nonce.
执行JS
nonce已知之后可以直接在自己VPS上发送请求创建笔记。
接下来要让受害者加载页面才行。这个只需要看share_read.js
就会发现触发hashchange
事件即可。由于缓存(前进后退缓存,bfcache)的存在,我们可以在已知nonce的标签页随便开个页面然后extra.history.go(-1);
,并修改hash值便能再次触发post加载,让已经被回调函数删掉的事件监听器在重新运行的js中焕发第二春。
这时我们发现单纯插入script标签并不能被执行,因为js脚本是往页面DOM节点写入innerText
而已。
经过一番神秘的摸索,大战MDN的manual,我们发现了一个神奇的属性:<iframe srcdoc="...">
在CSP测试网站中是这么说的:
1 | Content Security Policy: the srcdoc attribute in the iframe tag is not governed by frame-src, but its content is blocked by other directives of the parent page |
也就是插入时就会执行。至此,我们得以执行插入的js代码。
备注
官方idea来自这篇文章,也是场上找到过的材料:Chrome Cache to XSS
其他一些很有用的材料:Collection of CSP bypasses
CSP相关测试的网站:https://csplite.com/csp/
随机数预测解法的完整服务端代码:
1 | #!/usr/bin/python3 |
附官方WP(使用socket进行通信持续控制页面,基于事件触发需要的更新,攻击者动态返回所需CSS):
1 | from flask import Flask,request |
乐子
很多队都是使用随机数预测直接破的。主办方表示:
0day
利用路径穿越写maildev的lib/router.js增加自定义路由。
调用:把服务打挂(例如试图写/flag或者/etc/passwd什么的),重启的时候就会加载。