name: ssrf-testing description: 检测 SSRF 风险;当目标存在服务端发起请求的功能(URL 参数、webhook、预览/导入外部资源)时触发;适用于链接预览、文件导入、代理请求等场景。 when-to-use: 当目标存在服务端发起请求的功能(URL 参数、webhook、预览/导入外部资源)时 allowed-tools: bash,read_file,list_files,rg user-invocable: false
SSRF 检测(服务端请求伪造)
成因引用
SSRF 成因:source(用户可控的 URL / 主机 / 协议字符串)→ sink(服务端发起 HTTP 请求或其他网络协议请求的代码:http.Get / requests.get / URL.openConnection / curl_exec / 任意服务端发起的网络访问)。无白名单或协议 / IP 校验缺失时,攻击者可让服务端代为访问内网或外部任意目标。详见同根目录 pentest/web-security-testing/SKILL.md 漏洞成因图谱 · SSRF 行(不在本 skill 重复成因)。
关键 sink 形态:服务端拿到用户提供的字符串后,把它作为 URL 主机名 / 协议 / 路径直接发起网络请求。无论业务命名是"链接预览""头像导入""OAuth 回调""RSS 订阅""文档预览""JSONP 抓取",只要数据流终点是服务端发起的外部请求,都属同 sink。
触发线索(基线检查项)
以下是已知的常见 SSRF 触发线索,作为基线起点而非必检硬清单:
- 适用且已完成 → 标注
[x] done - 明确不适用 → 标注
[-] n/a (原因) - 基线未列出但实际发现 → 新增条目并标注
[+] added (来源)
基线触发线索按 "sink 语义" 分类(不按业务命名):
- Webhook / Callback 配置:用户填写 URL 让服务端在事件触发时去回调
- 头像 / 图片 URL 导入:用户给一个图片 URL,服务端下载后缩略 / 存储
- 链接预览 / 文档预览 / OG 信息抓取:服务端抓取目标 URL 生成预览卡片
- RSS / Atom / OPML 订阅源:用户给 feed URL,服务端定时抓取
- OAuth callback / 第三方登录回调:回调 URL 由参数控制
- PDF 生成 / 文档转换:服务端读取 HTML / 引用外部资源生成 PDF(背后可能是 wkhtmltopdf / headless 浏览器)
- JSONP / 代理请求 / fetch 代理:参数
url=/proxy=/target=让服务端代为请求 - DNS 解析 / 网络诊断:服务端解析用户传入域名 / 反查
- OpenAPI / Swagger 文档 URL 抓取:用户传入 schema URL 让服务端拉取
- 任何接收 "URL / 主机 / 协议字符串" 作为参数的端点:但不限于这些字段名——按 sink 语义判定
- 代码模式:后端代码出现
http.Get(userInput)/requests.get(userInput)/URL.openConnection()/curl_exec/ 任何服务端发起的请求调用 + 缺白名单 IP / 协议校验
思考检查点
加载本 skill 时按这些问题思考:
- 这个端点的服务端处理是否会发起任何外部网络请求?(哪怕只为生成缩略图、抓 favicon)
- 入口字段是否可控?参数命名"不像 URL"但其内容是否会被服务端拼接成 URL?(如只传
host+path两个参数被服务端拼成 URL) - 服务端是否限制了目标 IP / 协议 / 端口?还是直接相信用户输入?
- 子系统级 vs 端点级:多子系统是否有独立的请求出口、独立的 SSRF 防护?每个子系统的 SSRF 入口都需独立测
- 该端点的响应是否会回显请求结果?(决定属于回显型还是带外型 SSRF)
前置条件与安全边界
- 仅在授权环境测试。
- 单功能最多 15 次请求。
- 内网探测仅用于证明 SSRF 可达性,不进一步利用(如 Redis 写入、云元数据窃取仅作为危害说明,不真去拉敏感凭证)。
- 使用自有受控服务器接收外带请求;不向第三方公共服务发起非必要请求。
- 不向目标内网服务做破坏性写操作;按报告口径登记证据即可。
检测步骤
Step 1:外带确认(Out-of-Band)
- 将参数值设为自有受控服务器的 URL(如 Burp Collaborator、自建 HTTP 服务器)
- 确认自有服务器是否收到来自目标的请求
- 这是确认 SSRF 存在的最可靠方法——即使响应无回显,外带也能证明服务端真的发起了请求
记录外带请求的 User-Agent / 源 IP / Header 特征,有助于推断后端框架。
Step 2:内网探测
在外带确认后,逐步探测内网可达性:
| 目标 | payload | 确认方式 |
|---|---|---|
| 本地回环 | http://127.0.0.1、http://localhost |
响应差异(内容/状态码/耗时) |
| 云元数据 | http://169.254.169.254/latest/meta-data/ (AWS) / metadata.google.internal (GCP) |
响应中含实例信息 |
| 内网服务 | http://192.168.x.x:port / http://10.0.0.x:port |
端口开放时响应差异 |
| 容器 / K8s | http://kubernetes.default.svc |
API server 响应差异 |
Step 3:协议探测
测试是否支持非 HTTP 协议:
file:///etc/passwd(本地文件读取,但 SSRF 主体仍是 HTTP 协议本身的内网穿透能力,file:// 只是副作用)gopher://(Redis/Memcached 利用)dict://(服务探测)ftp:///ldap://(取决于客户端库支持范围)
Step 4:绕过测试
若直接 URL 被拒绝,尝试常见绕过:
- IP 变形:
0x7f000001、2130706433(十进制整数)、017700000001(八进制) - IPv6 变体:
[::1]/[::ffff:127.0.0.1] - DNS 重绑定:指向 127.0.0.1 的自有域名 / 时间窗口切换响应的解析器
- URL 混淆:
http://attacker.com@127.0.0.1、http://127.0.0.1.nip.io、http://127.0.0.1#@evil - 重定向链:自有服务器返回 302 重定向到内网地址(依赖目标客户端是否跟随重定向)
- URL 编码 / 双重编码绕过黑名单
示例库
正例形态(代码层根因)
resp, _ := http.Get(c.Query("url"))— Go 直接用 query 参数发起请求(url-server-fetch-ssrf)requests.get(webhook_url, timeout=5)— Python 拿 webhook 配置直接 fetch(webhook-callback-ssrf)img := download(c.PostForm("avatar"))— 用户传 URL,服务端下载并存储(avatar-image-import-ssrf)fetch(oauth_redirect_url)— OAuth 回调流程把 redirect_uri 直接当 URL(oauth-callback-redirect-ssrf)wkhtmltopdf userHtml outputPdf中userHtml含<img src="http://internal/...">— PDF 生成器跟随资源链接(pdf-resource-fetch-ssrf)
窄化反例(必须避免)
以下是 SSRF 维度的典型窄化误判:
- "参数名不像 URL(如
host=/id=/target=)→ 跳过 SSRF" — 错。按 sink 语义(任何参数被服务端用于发起请求都是候选),不按参数命名筛选。常见反例:参数叫host与path,服务端把它拼成 URL 后 fetch - "已测了 webhook → 跳过头像上传" — 错。每个 SSRF 入口独立结账,不同入口的过滤逻辑、白名单、重定向跟随策略可能完全不同
- "服务端有 IP 黑名单 → 安全" — 错。DNS 重绑定、IPv6 变体、URL 编码、十进制 / 八进制 IP、重定向链都可能绕过黑名单。黑名单是必须破的边界条件,需独立尝试多种绕过 payload
- "已在子系统 A 命中 SSRF → 子系统 B/C/D 也假定有 / 没有" — 错。跨子系统独立结账,命中不可外推、安全也不可外推
- "URL 不能含
file://→ 安全" — 错。HTTP 协议本身就能内网穿透,file://只是 LFI 副作用。SSRF 的主危害在于 HTTP 协议下的内网访问 + 云元数据窃取,不依赖非 HTTP 协议 - "响应无回显 → SSRF 无危害" — 错。盲 SSRF 仍可:a) 外带回连证明 b) 利用云元数据获取临时凭证 c) 探测内网开放端口 d) 触发内部服务的副作用(如 Redis 写入)
反例义务(必须遵守)
为什么这里是「必须」:反例义务属于交付契约——"该子系统 SSRF 已防护"结论是覆盖完整性的产物声明,缺失反向验证清单会让下游误信"该维度全站安全"。
写"未发现 SSRF"或"已防护"前,产物必须包含:
- 测过的 SSRF 候选端点完整清单(按 sink 语义枚举:所有可能让服务端发起外部请求的端点;多子系统场景按子系统独立分组结账)
- 每个端点尝试的 payload 形态(外带 URL / 127.0.0.1 / 云元数据 / IP 变形 / DNS 重绑 / 重定向链 / URL 混淆 / 非 HTTP 协议)
- 每个端点的响应证据(外带回连记录 / 内网探测响应差异 / 状态码 / 错误信息)
- 跨子系统独立结账记录(每个有"服务端外发请求"能力的子系统都独立结账)
清单不完整 → 结论降级为 partial-coverage 并显式声明未覆盖范围。
闭环验证要求(必须遵守)
通用闭环口径见同根目录 common/closure-verification.md(技能表 path 列同一抽取根下,需要时 read_file 读取)。核心:结论须形成「输入 → 处理 → 真实危害 → 可复核证据」完整证据链;仅凭状态码差异等中间信号最多判 suspected,证明服务端确实代为发起请求(外带回连 / 内网响应内容泄露)才判 confirmed。
实际效果验证方向(至少证明一类)
- 外带:自建服务器收到目标发起的请求,源 IP / User-Agent 与目标后端特征一致
- 内网响应内容:回显中包含内网服务的真实响应(如 169.254.169.254 元数据、内网 admin 页面 title)
- 端口探测的稳定时间差或状态码差异,多次复验可复现
判定标准
| 现象 | 判定 |
|---|---|
| 服务端向攻击者控制的服务器发起请求(外带确认),或响应内容包含内网服务信息 | confirmed |
| 不同 URL 产生不同响应(时间/状态码/大小)但未获得外带确认或内容泄露 | suspected |
| URL 参数被严格校验(白名单 + 协议限制 + IP 解析后校验),多类绕过 payload 均无效 | not vulnerable |
| 只测了部分 SSRF 入口 / 仅试一种绕过 payload 类型 | partial-coverage(不得宣称 safe) |
修复建议
- 对 URL 参数做白名单域名 / IP 校验,且在 DNS 解析之后再校验(防止 DNS 重绑定)
- 禁用非 HTTP(S) 协议(在客户端层显式拒绝
file:///gopher:///dict://等) - 使用独立网络出口(无内网访问权限)发起外部请求;或通过明确的代理网关统一管控
- 禁止请求跟随重定向;或在重定向后重新校验目标地址
- 禁止访问云元数据地址(
169.254.169.254/metadata.google.internal/ 容器内部 service DNS) - 对头像 / 文档预览类下载,限制响应体大小、超时、内容类型,避免被作为内网扫描通道