资料说明:原记录中提及的本机脚本及题目附件未随本文附上,相关文件名仅用于说明当时的复现流程。
Crossed Signals(Northline Service Desk)· 四层门,全靠“同一个值传两遍、两侧不同源”
平台题目名:Crossed Signals(Web,655 分,10:19:33 提交)。内部代号
northline,目标zu9tte01。
1 | flag{IcP9omrAE0EKo5gRrcUafl8LYiyFaEH1cD4tuj3F} |
目标:http://zu9tte01.nestctf.cyberpeace.cn:58080/(内网 172.78.112.236)
凭据:advisor@northline.test / Service-Desk-2026(题面给的服务台账号)
复现:py -3.13 solve_final.py
一句话
整题的骨架是 “同一个语义值在请求里出现两次,而校验与执行各自只看其中一次”:VIN 走 HTTP 头与 JSON body 各一份、工具名走 Mcp-Name 头与 params.name 各一份、bundle 的 release 状态走 notes 接口的批量赋值。三处错位叠起来,把受限车辆的 private/recovery.txt 从封存包里取了出来。
0. 攻击面
| 通道 | 说明 |
|---|---|
| REST | POST /api/login、GET /api/me、GET /api/cases、POST /api/bundles/<id>/notes、GET /api/bundles/<id>/download |
| MCP | POST /mcp,JSON-RPC 2.0,MCP-Protocol-Version: 2026-07-28,工具参数 schema 里带 x-mcp-header 映射 |
登录后 /api/me:
1 | {"user":"advisor@northline.test","role":"service_advisor","fleet":"West depot", |
/api/cases 里目标工单 SR-26042:VIN CTF6SVC0000000002、access:"restricted"、fleet:"Factory validation"、status:"Security review"。页面自己都写着:
Cross-fleet case metadata is visible; vehicle data is restricted.
tools/list 给三个工具:get_vehicle_health、get_update_status、export_case_bundle(vin, case_id, diagnostic_ref, evidence_ref)。关键在于每个参数 schema 都标了 "x-mcp-header": "VIN" —— 这就是“同一值两份”的伏笔。
1. 门一:VIN 头/体错位
前端 /app.js 的 sendMcp():
1 | request.params.arguments = {vin: record.vin}; |
官方客户端永远让头和体相等,所以没人会发现网关只认头。把头填自己被分配的 VIN、体填受限那台车的 VIN:
1 | POST /mcp |
拿到两个令牌:
1 | diagnostic_ref = DX-eba5efee02b906534298210d |
坑:_meta 的三段元数据缺一个就 -32600 Invalid request metadata;Mcp-Method 和 body 里的 method 不一致则 -32020 HeaderMismatch。
2. 门二:工具 scope 也看头不看体
export_case_bundle 是 security operations 专用,直接调 → 403 {"error":"tool_scope_denied"}。
网关按 Mcp-Name 头判 scope,执行器按 params.name 派发:
1 | Mcp-Name: get_vehicle_health # 网关眼里这是只读工具 |
→ 200,返回 bundle_id 和一段 base64 的 SR-26042-advisor-preview.zip。预览包的 manifest.json 老实列出了三条:
1 | "entries": ["diagnostics/telemetry.json", |
但预览 zip 只有前两条(private/ 被裁掉),且 release_state: "pending" —— 预览包泄露了私有条目名,这是第二个设计问题。
3. 门三:release 状态靠批量赋值
GET /api/bundles/<id>/download → 403 {"error":"bundle_release_pending","required_release_state":"approved"}。
服务端没有 approve/release 端点,但 notes 接口把请求体整个回写进 bundle 记录:
1 | POST /api/bundles/<id>/notes |
(只发 {"note":"x"} 是 200 但状态仍是 pending;release_state / state / status / approve / action / role / scope 任一都能被吸收成 approved。)
4. 开箱
1 | GET /api/bundles/<id>/download -> 200,真 zip(1287 B) |
1 | private/recovery.txt |
5. 脚本
solve_final.py 一条链跑完:登录 → 头/体错位取两个 ref → Mcp-Name 错位导出 → notes 批量赋值批准 → 下载解压 → 正则取 token。二次运行 token 稳定。
6. 顺手记下的接口细节
| 端点 | 行为 |
|---|---|
POST /api/login |
{username,password} → {token,expires_in,role};token 是 32 字节 base64url,不是 JWT |
GET /api/me |
含 assigned_vins、tool_endpoint、protocol_version |
GET /api/cases |
跨车队元数据可见,access 区分 assigned / restricted |
POST /api/bundles/<id>/notes |
note 必填({} → invalid_note),其余任意字段被回写进记录 |
GET /api/bundles/<id>/download |
需 release_state=approved |
PATCH / PUT |
501(服务端只实现 GET/POST) |
resources/*、prompts/* |
unsupported_method(网关只做 tools) |
7. 踩坑
| 坑 | 现象 | 解决 |
|---|---|---|
_meta 元数据 |
-32600 Invalid request metadata |
三个 io.modelcontextprotocol/* 字段一个都不能少 |
Mcp-Method 与 body 不符 |
-32020 HeaderMismatch |
头与 body 的 method 必须一致 |
| 只填头不填体 | 仍然 403 | 两处都要改:头放自己、体放目标 |
| notes 只发 note | 200 但仍 pending | 必须显式塞 release_state:"approved" |
8. 修复建议
- VIN 只保留一个真源:只信
Mcp-Param-VIN或只信 body,二者不一致直接 400。 - 工具 scope 用 body 的
name判,不要用可伪造的头。 - notes 接口做字段白名单,
release_state只能由服务端状态机推进(且需要 security 角色)。 - 预览包不要和完整包共享同一份 manifest,私有条目名本身就是信息泄露。
9. 一句话总结
MCP 这类“头 + JSON-RPC body”双通道的接口,天生适合出参数错位题:网关为了做鉴权/限流会把关键参数提到头里,而执行层又从 body 里取。只要两处不是同一个来源,就一定有一处是假的。
本文作者 Allureluoli
文字版权归作者所有,转载请注明出处;商业用途请先联系二月。