ssrf-testing

star 72

检测 SSRF 风险;当目标存在服务端发起请求的功能(URL 参数、webhook、预览/导入外部资源)时触发;适用于链接预览、文件导入、代理请求等场景。

Q16G By Q16G schedule Updated 6/7/2026

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)

  1. 将参数值设为自有受控服务器的 URL(如 Burp Collaborator、自建 HTTP 服务器)
  2. 确认自有服务器是否收到来自目标的请求
  3. 这是确认 SSRF 存在的最可靠方法——即使响应无回显,外带也能证明服务端真的发起了请求

记录外带请求的 User-Agent / 源 IP / Header 特征,有助于推断后端框架。

Step 2:内网探测

在外带确认后,逐步探测内网可达性:

目标 payload 确认方式
本地回环 http://127.0.0.1http://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 变形:0x7f0000012130706433(十进制整数)、017700000001(八进制)
  • IPv6 变体:[::1] / [::ffff:127.0.0.1]
  • DNS 重绑定:指向 127.0.0.1 的自有域名 / 时间窗口切换响应的解析器
  • URL 混淆:http://attacker.com@127.0.0.1http://127.0.0.1.nip.iohttp://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 outputPdfuserHtml<img src="http://internal/..."> — PDF 生成器跟随资源链接(pdf-resource-fetch-ssrf

窄化反例(必须避免)

以下是 SSRF 维度的典型窄化误判:

  • "参数名不像 URL(如 host= / id= / target=)→ 跳过 SSRF" — 错。按 sink 语义(任何参数被服务端用于发起请求都是候选),不按参数命名筛选。常见反例:参数叫 hostpath,服务端把它拼成 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)
  • 对头像 / 文档预览类下载,限制响应体大小、超时、内容类型,避免被作为内网扫描通道
Install via CLI
npx skills add https://github.com/Q16G/aster --skill ssrf-testing
Repository Details
star Stars 72
call_split Forks 6
navigation Branch main
article Path SKILL.md
More from Creator