知道 MCP 和 A2A 有什么暴露面问题之后,接下来的问题是:怎么系统地找出来。这篇讲 AgentScan 是怎么做的。
流水线概览
目标解析
↓
TCP 端口扫描
↓
HTTP/HTTPS 可达性过滤
↓
协议探测(MCP / A2A / LLM 并行)
↓
只读能力枚举
↓
报告生成(terminal / JSON / HTML)
每一层的输出是下一层的输入。端口扫描过滤掉不可达的目标,HTTP 过滤过滤掉非 HTTP 服务,协议探测在剩余候选上做指纹识别。
目标解析
支持格式:IP、CIDR、IP range、domain、host:port、URL。
192.168.1.1
10.0.0.0/24
10.0.0.1-10.0.0.50
example.com
example.com:8000
https://api.example.com/mcp
URL 输入会保留 scheme、port 和 path。https://api.example.com/mcp 会:
- 优先探测
/mcp路径 - 在 HTTPS 上做 SNI(用 hostname)
- 如果
/mcp没命中,再试内置路径字典
这对反向代理或自定义挂载路径很有用——如果你知道服务挂在哪,可以直接告诉 AgentScan。
TCP 端口扫描
用 Go 标准库做并发 TCP 连接,默认并发 500,超时 2000ms。
端口字典是分协议的:
- MCP 端口字典(框架默认端口、常见开发端口)
- A2A 端口字典
- LLM 端口字典(Ollama
11434、vLLM8000等)
scan 命令会取三个字典的并集。--skip-port-scan 跳过这一层,把输入直接当作已开放的 host:port。
HTTP/HTTPS 过滤
TCP 开放之后,AgentScan 做一次 HTTP 连接尝试:
- 根据端口推断 scheme:
443、8443默认 HTTPS;其他端口先试 HTTP,失败再试 HTTPS - 记录
Server和Content-Type响应头 - 过滤掉无法以 HTTP 连通的端口
这一层的目的是在进入协议探测之前减少候选数量。一个开放的 22 端口(SSH)不需要进 MCP 探测。
MCP 探测
这是 AgentScan 里最复杂的部分。原因是 MCP 有两套传输协议,而且指纹识别不是简单字符串匹配。
两种传输
Streamable HTTP(2025-03-26 规范):
直接 POST initialize 到候选路径:
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"agentscan","version":"1.0"}}}
响应可以是 JSON 或 SSE 流(规范都允许)。
HTTP+SSE legacy(2024-11-05 规范):
分两步:
- GET
<sse_path>,建立 SSE 长连接,等待endpointevent - 从 event data 里拿到 POST 路径,再 POST initialize
- 响应通过 SSE 流推回来
GET /sse
data: /messages/?session_id=abc123
↓
POST /messages/?session_id=abc123
↓
SSE stream: {...initialize response...}
AgentScan 对每个候选路径并发尝试两种传输,优先返回 no-auth 命中。
指纹评分
找到响应之后,AgentScan 不是做字符串匹配,而是组合评分:
| 信号 | 分值 |
|---|---|
result.protocolVersion 存在 | +0.2 |
result.capabilities 含 MCP 特有 key(tools、resources、prompts、sampling) | +0.3 |
result.serverInfo.name 存在 | +0.1 |
Mcp-Session-Id 或 Mcp-Protocol-Version 响应头 | 兜底确认 |
LSP(Language Server Protocol)排除机制:如果 capabilities 里出现 LSP 特有 key(textDocumentSync、hoverProvider 等),直接返回 0,避免把 LSP 当 MCP。
原因是 MCP 和 LSP 都是 JSON-RPC,响应结构类似,如果没有 LSP 排除,会有误报。
阈值 0.35。低于阈值的响应不报告。
no-auth 判断
状态码 200 且指纹评分过阈值 → no-auth。
4xx 响应会做进一步分析:
- 有
Mcp-Session-Id/Mcp-Protocol-Version响应头 + 401/403 → auth-required - 响应体含
"jsonrpc"且有"error"字段 + 401/403 → auth-required - 其他 4xx → 不报告
这区分了"MCP Server 在但需要认证"和"不是 MCP 服务"。
只读枚举
确认 no-auth 后,执行:
tools/list
resources/list
resources/templates/list
prompts/list
不调用 tools/call,不写远端资源。AgentScan 是只读的。
蜜罐信号
两个场景会记录 honeypot signal:
- 发送无效协议版本(随机字符串),服务器依然接受
- 两次 initialize 返回相同 Session ID
这些信号不过滤结果,用 --exclude-honeypots 才过滤。
A2A 探测
A2A 的探测比 MCP 简单,核心是 GET Agent Card,然后做多维度评分。
获取 Agent Card
优先尝试:
/.well-known/agent.json
/.well-known/agent-card.json
/agent.json
如果用户输入了 URL path,会基于该 path 推导 card 路径(比如 /agents/my-agent 会尝试 /agents/my-agent/.well-known/agent.json)。
指纹评分
解析 JSON 之后做多维度评分,信号包括:
name字段存在(Agent 名称)url字段是有效 URLskills数组非空capabilities对象存在securitySchemes/security字段- 响应
Content-Type是application/json - 路径是
.well-known路径
有负面信号会跳过:比如响应里同时有明显的非 A2A 标志(比如 OpenID 配置字段)。
置信度阈值两档:
confirmed(≥ 0.65):直接报告probable(≥ 0.45):只在--include-probable时报告
JSON-RPC 可达性检查
拿到 Card 里的 url 字段之后,AgentScan 做轻量状态探测:
- 向
url发一个最小 JSON-RPC 请求 - 判断响应是 200 no-auth、4xx auth-required、连接失败、还是端点关闭
同时检查:
- 私网地址泄露(Card 里的
url或additionalInterfaces包含 RFC 1918 地址) - 跨主机接口(Card 里声明了不同主机的接口地址)
这两类检查对应 A2A 文章 里讲的内网地址泄露问题。
LLM 推理 API 探测
LLM 模块用 YAML 模板驱动,内置覆盖了主流框架:Ollama、vLLM、SGLang、TGI、llama.cpp、Xinference、LiteLLM、FastChat、LocalAI、LM Studio、LMDeploy。
模板结构
info:
name: ollama
severity: high
priority: 50
http:
- method: GET
path: /api/tags
matchers:
- 'status == 200 && json.has("models")'
models:
- method: GET
path: /api/tags
extractor:
part: body
type: json_array
json_path: models[*].name
auth:
endpoint: /api/tags
open_status: [200]
auth_status: [401, 403]
模板可以定义:
- 一阶段 HTTP matcher(判断是不是这个框架)
- 二阶段确认探测(减少误报)
- 版本提取
- 模型列表提取
- 认证状态判断
- negative matcher(排除相似但不是的服务)
- priority(多框架歧义时谁优先)
自定义模板
agentscan llm example.com --template-dir ./my-templates
同名 info.name 会覆盖内置模板。如果有内部 LLM Gateway 或私有推理框架,可以写自己的模板。
输出
三种格式:
terminal:适合现场看,带 ANSI 颜色,按协议分块输出,no-auth 高亮。
JSON:机器可读,包含完整 evidence 字段(指纹信号、HTTP 状态、响应头),适合自动化处理和二次分析。
HTML/TXT:适合报告和交付,scan 命令自动生成,按 MCP/A2A/LLM 分 tab。
JSON 的 evidence 字段设计目标是让结果可复现——不只输出"命中",还输出是什么信号让它命中,支持人工二次确认。
前置阅读:MCP 暴露面的安全问题 · A2A 暴露面的安全问题
如何用 AgentScan 扫你的环境:使用说明