接到一个授权测试目标,业务方给的范围是一个文档预览服务。这类服务十有八九会把http://开头的参数直接透传给后端去抓取,所以我穷举了所有可能接收 URL 的字段,找出三个入口,替换成内网探测用的地址,结果其中一个入口直接把内网的响应内容原样吐了回来。
这一趟之后我彻底意识到一个事实:通用漏洞扫描器解决不了 SSRF 的检测问题。它们知道“请求发出去了”,但不知道“请求从哪里回来”“请求返回的内容意味着什么”“这条链路上还能不能继续打通”。这篇文章把我在 SSRFScanner 这个项目上的设计思路、实现细节、以及实战中总结的经验完整梳理一遍,写给正在做安全测试和扫描工具开发的朋友。
1. SSRF 扫描器要解决的真实痛点
1.1 SSRF 漏洞到底寄生在哪些场景里
SSRF(Server-Side Request Forgery,服务器端请求伪造)检测难,首先难在它不是一个“输入框加后端 SQL 拼接”式的漏洞。只要业务逻辑里存在“由服务器去请求一个由用户控制的地址”的动作,就可能存在 SSRF。
我在实际项目里见过的高频寄主场景有这么几类:社交软件里的 URL 预览、链接卡片;在线文档或图片处理工具的“导入 URL”功能;Webhook 配置面板;PDF 生成器(把网页转成 PDF 的服务);还有各类代理抓取型 API。
这类场景在自动化扫描器眼里往往表现为一个正常的 200 响应,没有明显错误信息,很难判断后端是否真正发起了网络请求。这也是为什么很多通用扫描器对 SSRF 基本无能为力——不是引擎不行,而是检测模型从一开始就不是为“请求方在服务端”设计的。
1.2 通用扫描器在 SSRF 检测上的三个盲区
用通用扫描器扫 SSRF,通常会遇到三个绕不开的盲区:
盲区一是不验证带外通道。很多扫描器只拿到一个“响应成功或失败”的状态就完事了,根本不会去验证一个域名是否真的被服务端请求过。SSRF 的核心证据恰恰在请求链路本身,而不在响应码里。
盲区二是不跟踪跳转链路。开放重定向、302 跳转、多级转发,这类场景在 SSRF 里太常见了,但通用扫描器往往是单请求单响应模型,跳转链路一深就跟丢了。
盲区三是不进行多步闭环验证。就算扫描器确认服务端能请求某个地址,它也不会继续判断“这个地址是不是内网 IP”“内网里跑的是什么服务”“这个服务是否存在可被利用的高危组件”。而这恰恰是客户最关心的。
我给两张工作模式做过一个直观对比,放在下面:
| 检测维度 | 通用扫描器 | SSRFScanner |
|---|---|---|
| 请求是否真正发出 | 看状态码猜 | 结合带外响应与响应体判断 |
| 能否访问内网 | 基本不验证 | 内置多规则内网地址探测 |
| URL 跳转链 | 跟随有限次数就断 | 完整记录跳转链并标记目标属性 |
| 攻击链闭环 | 无 | 自动探测端口、识别服务指纹 |
| 结果可信度 | 需要人工二次确认 | 输出证据链,可直接进报告 |
通用扫描器适合挖“一锤子买卖”的漏洞,比如 SQL 注入、XSS,但 SSRF 是一个需要多步验证的漏洞类型,工具的逻辑必须跟着这个特点走。
2. SSRFScanner 的模块拆解与部署
2.1 六大核心模块的职责划分
SSRFScanner 整体是模块化设计的,这样做的原因很实际:SSRF 检测链路太长,如果所有逻辑混在一个文件里,后面加一个新协议或新指纹类型,就得动主流程,容易改崩。拆成模块之后,每一段职责清晰,测完一块再动下一块。
第一块是目标归集模块。它负责从目标列表、爬虫结果或 Burp 导出的数据里,把可能接收 URL 的参数位全部提取出来。这个模块我做了两层清洗:先把明显不是 URL 参数的字段过滤掉,再把重复参数去重,免得后面请求引擎做重复功。
第二块是请求引擎。基于 asyncio 实现的异步请求层,负责实际发包、超时控制、重试、跟随跳转,以及记录每条请求的完整链路信息。
第三块是探测向量库。这是检测能力的核心资产,里面存的是所有绕过思路和探测 payload,包括内网 IP 变体、短格式 IP、八进制十六进制写法、协议切换、关键云元数据地址等等。
第四块是响应分析模块。拿到响应之后,不只是看状态码,还要做内容判断:响应体里有没有内网服务指纹、有没有云元数据特征、有没有重定向到内网地址的 Location 头。
第五块是攻击链验证模块。发现某个入口能访问内网后,自动对目标内网 IP 做轻量端口探测和服务指纹识别,给出“这条链路的危害程度”判断。
第六块是报告输出模块。所有结果按证据链组织输出,每一条结论都附带请求包、响应包、跳转记录和对应的时间线。
2.2 环境部署与参数配置
部署过程非常简单,依赖基本都在 Python 3.9+ 生态里。
pip install -r requirements.txt python ssrfscanner.py -f targets.txt -o report.html --verify配置文件用 YAML 格式,几个关键参数的含义和调优经验我直接写在注释里:
http: timeout: 8 # 单请求初始超时,8秒是折中值 retries: 2 # 连接失败后的重试次数 max_redirects: 10 # 最大跟随跳转次数 user_agent: "Mozilla/5.0 ..." avoid_blocks: max_qps: 20 # 每host每秒最大请求数 random_delay_range: [0.5, 1.5] # 请求间隔随机化,防触发WAF sleep_after_detect: 3 # 发现内网服务后自动暂停3秒 callback: provider: "interactsh" # 带外回调通道 token: ""这里特别说一下max_redirects。SSRF 检测里跳转链接太长的情况不多,但一旦发生,基本都是多层代理转发或者 SSO 统一登录逻辑。设置 10 次足够覆盖绝大多数业务场景,又不会让请求无限追下去。
2.3 为什么选 Python 异步引擎
有人问过我为什么不用 Go 写扫描器。Go 并发确实强,但 SSRF 检测的高频依赖是数据分析和协议处理,Python 生态里现成的请求库、DNS 解析库、指纹库最丰富,做原型迭代最快。asyncio 加 aiohttp 处理几千个并发探测请求完全够用。
实际开发中最需要注意的反而是一个看起来不起眼的问题:异步下的超时处理一定要细粒度。DNS 解析超时要单独设,TCP 连接超时要单独设,读取响应体超时也要单独设。我最初把所有超时统一成一个值,结果在目标服务响应很慢但连接很快的场景下,大量请求被误判为超时,漏报率明显偏高。
3. 内网地址探测:规则、绕过与判定逻辑
3.1 内网地址判定的多级规则
内网地址探测定不能做成“解析一下 IP,匹配一下是不是内网段”这么简单。因为服务端发起请求的目标地址可能是个域名,域名背后可能是 CDN、可能是内网解析、也可能是 DNS 重绑定,IP 本身还能做各种格式变体。SSRFScanner 的内网地址判定分了三层:
第一层是纯 IP 规则。目标域名解析出来的所有 IP 逐项比对,基础规则包括 IPv4 私网段和 IPv6 的特殊地址段:
- 127.0.0.0/8 以及 ::1(本机回环)
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 169.254.0.0/16(云元数据地址段)
- fc00::/7 和 fe80::/10
第二层是域名语义规则。探测向量库里内置了一批解析结果指向内网地址的测试域名,请求引擎会把它们解析出的 IP 和响应结果都记录在案。
第三层是响应指纹规则。这一步最接近人工判断:拿到响应体后,判断是否包含内网设备的特征内容,比如路由器管理页面标题、打印机状态页、数据库管理面板等。如果一看到H3C、Cisco、MikroTik这类指纹,基本可以断定请求已经到达内网设备了。
3.2 容易被忽略的 URL 解析差异
服务端拿 URL 参数后通常会做一次合法性校验。很多开发者以为把127.0.0.1和localhost过滤掉就安全了,实际上 IP 地址在 URL 里的写法远不止一种。
我举几个实际用到的格式变体:
| 原始地址 | 变体写法 | 说明 |
|---|---|---|
| 127.0.0.1 | 2130706433 | 十进制整数格式 |
| 127.0.0.1 | 0177.0.0.1 | 八进制表示 |
| 127.0.0.1 | 0x7f.0.0.1 | 十六进制表示 |
| 127.0.0.1 | 127.1 | 短格式,省略后两段 |
| 127.0.0.1 | %31%32%37.0.0.1 | URL 编码绕过 |
扫描器在做内网地址探测时,必须覆盖这些变体。实现思路很简单:拿到目标地址后,先把各种格式统一规范成十进制 IP,再走内网规则匹配。
import ipaddress def normalize_host(raw): # 先尝试按普通IP解析 try: return str(ipaddress.ip_address(raw)) except ValueError: pass # 尝试十进制整数格式 try: packed = int(raw).to_bytes(4, 'big') return str(ipaddress.ip_address(packed)) except Exception: pass # 其他变体格式,交给更完整的解析库继续处理 return raw这类解析逻辑的价值在于:它把人工测试时积累的“经验型绕过”固化成了自动化规则。工具跑一轮,相当于一个熟悉各种绕过手法的测试人员把目标入口全部试了一遍。
3.3 云元数据地址的专项验证
169.254.169.254 这段地址在 SSRF 检测里是一个必须单独处理的项目。很多企业应用跑在云上,SSRF 一旦能访问到云元数据服务,攻击面会直接放大到云平台凭证层面。
SSRFScanner 里有一个单独的云元数据检测模块,会对这个地址发起有限数量的探测请求,并从响应内容判断是否命中元数据 API 特征。比如 AWS 的元数据服务响应里会出现ami-id这类字段名,不同厂商有自己的特征。同时,响应分析模块对这类内容做了自动脱敏处理,防止敏感信息原样写入报告。
这里要强调一点:云元数据检测请求的数量和频率需要严格控制,因为这类探测一旦打到厂商的元数据服务,特殊请求模式可能被平台侧风控注意到。默认配置是每个入口只对元数据地址发 2 个验证请求,拿到特征字段就停止。
4. URL 跳转检测:从开放重定向到 SSRF 的桥接
4.1 为什么 SSRF 扫描器一定要管跳转
一种很典型的场景是:目标存在一个开放重定向漏洞,redirect?to=xxx可以跳转到任意外部地址。如果这个跳转被某些内部服务当作合法跳转来跟随,攻击者就能用这个跳转作为 SSRF 的中转站。
而且实际业务中,SSRF 的入口往往不是一个直接传 URL 的参数,而是“先跳转再请求”的多步链路。比如参数 A 跳转到参数 B,参数 B 里的地址才是真正被服务端请求的目标。如果扫描器只检测单跳,这类链路会全部漏掉。
4.2 跳转链的完整记录方式
SSRFScanner 的请求引擎默认跟随跳转,但记录的是完整链路而不是最终状态。链路里每一个节点的响应码、Location 头、解析后的 IP、IP 是否命中内网规则,全部打包成一条记录。
这样做的好处是排障快。之前碰到过一个案例,报告显示某个参数可以跳转到内网地址,但实际手动复现时怎么都不成功。查链路记录才发现中间节点有一个外网 CDN 的 302 跳转,CDN 节点解析出的 IP 里碰巧出现了内网地址。把这个中间节点标记出来,问题定位就很清楚了。
4.3 跳转检测的三种策略
根据测试场景不同,跳转检测有三种可选策略:
策略 A 是不跟随跳转,只记录 Location 头内容。适合那种只需快速确认是否存在开放重定向的场合,速度最快,但没法确认跳转后的资源是否真的被请求。
策略 B 是跟随三次以内。适合大多数常规目标,既能确认跳转目的地,又不会陷入过深的链路。
策略 C 是智能跟随,对开放重定向参数的跳转结果做二次探测。比如发现redirect参数跳转到了外部地址,再对那个外部地址做一次内网探测,判断是否形成了可利用的跳板链。
实际扫描时,我通常把策略 C 作为验证阶段的手段,而不是初扫阶段就开启,否则请求量会成倍增加。
4.4 跳转误报控制经验
跳转类的误报通常来自两个地方。一是外网 CDN 加速节点跳转后 IP 解析成内网地址——某些 CDN 服务商在做回源时有内网转发,但这属于服务商内部行为,跟目标业务没有直接关系;二是统一登录系统的跳转,目标是一个集中认证平台,跳到哪里都不稀奇。
处理方式是在判断规则里加上下文:维护一份“可信域名/可信 CDN 段”的名单;跳转后的响应内容必须出现内网服务指纹或与目标业务相关的特征;同时检查跳转后的 Location 是否命中内网 IP 规则。三条件同时满足才判定为有效跳转,能压掉很大一部分假阳性。
5. 攻击链验证:从“可能”到“确认”
5.1 为什么普通扫描报告没有说服力
“该参数疑似存在 SSRF”是安全测试项目里最没说服力的一句话。客户看到这句话后的第一反应一定是:你说能访问内网,然后呢?能拿到数据吗?能进一步控制服务器吗?
所以 SSRFScanner 要把验证往前推一步:发现某个入口可以访问内网时,自动对可访问的内网 IP 做轻量端口探测,识别常见的数据库、缓存、管理面板等服务指纹,判断是否存在可以直接利用的高危组件。
需要明确的是,工具只做“指纹识别 + 利用条件判断”,不主动执行破坏性利用动作。这个边界在测试场景里很重要——授权测试的目的是证明漏洞存在和影响范围,而不是替攻击者把路走完。
5.2 攻击链验证的三层模型
整个验证流程按三层递进:
| 层级 | 验证目标 | 典型证据 |
|---|---|---|
| 第一层 | 入口是否真正发起到内网的请求 | 带外响应、响应体回显 |
| 第二层 | 内网可达性的具体范围 | 端口开放情况、服务指纹 |
| 第三层 | 目标服务是否存在已知可利用条件 | 组件版本、未授权访问特征 |
第一层解决“有没有”的问题,第二层解决“能到哪里”的问题,第三层解决“危害有多大”的问题。三层都过了,报告里就能理直气壮地写“确认存在 SSRF 导致的严重风险”。
5.3 一个实际验证流程的复盘
用一个小例子展示完整流程。某个在线文档导入功能,参数是document_url。SSRFScanner 初扫时发现该参数可以访问一个内网地址,响应体里出现了特定设备的 HTTP 响应头。
攻击链验证模块随后对这个内网 IP 的常见端口做了轻量扫描,发现 8080 端口开放,返回内容里的 favicon hash 和登录页标题指向一个协作平台。工具把这三层证据组织成一条链路:
document_url 参数可请求任意地址 └─> 内网 IP 可达(响应头特征确认) └─> 8080 端口开放,指纹识别为协作平台 └─> 登录页特征匹配,存在已知高危版本特征这一步没有执行任何攻击行为,但报告已经可以明确写出风险等级和影响面。测试人员拿到这份证据后,再手动做最终确认和利用验证,整个过程高效多了。
我之前用 CTFHub 技能树的 SSRF 关卡做过回归测试。通过在线靶场跑一遍全量检测流程,能快速验证工具的检测覆盖率有没有在迭代中回退。我的做法是每更新一次探测向量库或响应指纹库,就把靶场里的 SSRF 相关题目重跑一遍,逐题对照结果,保证改动不影响已有检测能力。
6. 实战踩坑与调优记录
6.1 超时设置不是越大越好
SSRF 检测里,超时设置直接决定漏报率和扫描时长这两个维度。如果目标服务处理请求很慢,超时太短会漏报;但如果统一设成很大,目标几千个参数并发扫下来,时间成本会高到没法接受。
我目前的经验值是:初始扫描超时设 6 秒,在确认存在内网地址过滤或目标访问较慢的场景下,单独拉长到 12 秒重测一轮。第一轮保证覆盖率,第二轮保证准确性,两轮加起来的时间也远小于把所有请求全部用长超时跑一遍的时间。
6.2 别把目标站点打到熔断
用扫描器最忌讳的就是拿最大并发量对着目标狂轰一气。SSRF 探测请求比普通漏洞扫描更容易触发 WAF 和限流,因为内网探测请求在业务日志里看起来极其可疑——一个来自外网的会话,反复请求各种内网地址和特殊格式 IP。
SSRFScanner 内置了 per-host 的 QPS 限制,默认 20 QPS,单个参数默认间隔 0.8 秒,一旦检测到内网服务响应,自动暂停后续探测 3 秒。这个机制是在一次真实测试里被教育出来的:当时没加限速,目标网关的 WAF 直接把整个业务 IP 封了半小时,测试从技术问题变成了事故。
6.3 DNS 解析结果要分层缓存
这个坑我踩得比较深。扫描刚开始时,把目标域名解析成 IP 缓存起来直接复用,能大幅度提升速度。但 SSRF 检测里,域名解析结果的变化恰恰是判断 DNS 重绑定和 CDN 调度的关键信号。
如果所有域名都走同一个长缓存,DNS 重绑定场景根本测不出来;如果所有域名都不缓存,几千个请求全走完整 DNS 解析,性能又回不去。折中方案是把 DNS 缓存分两层:业务域名走长期缓存,探测专用的特殊域名走短期缓存、逐次解析。这样既保证性能,又不会错过解析层的变化。
6.4 在云上部署扫描器的误报处理
很多人会在云服务器上跑扫描器,这时候很容易发现扫出来的结果一片红。原因在于云厂商网络里,历史遗留的“脏 IP”会被广播到同一段,某些现象会导致 DNS 解析结果命中云厂商的内网保留段,产生大量误报。
处理方式是在配置里加入“云厂商元数据地址黑名单”,结合 DNS 解析结果做二次确认:只有命中内网 IP 规则、同时响应体出现明确内网服务指纹的才判定为有效结果。单靠 IP 段匹配,云上环境跑出来的报告根本没法看。
7. 用久了之后,我对服务端 SSRF 防护的几个观察
7.1 只封 127.0.0.1 等于没封
服务端最常见的错误是只过滤了127.0.0.1和localhost。实际上,我统计过 SSRFScanner 跑出的有效内网地址里,10.x、172.x、192.168.x这些内网段的占比远高于本机回环地址,云元数据地址也不算罕见。开发者做 URL 合法性校验时,至少应该做到 IP 段级别过滤,而不是只过滤两个最常见的名称。
7.2 先解析再判断,而不是先判断再解析
正确的防护顺序应该是:拿到来访 URL 后,先通过 DNS 解析出真正的 A 记录,拿到解析后的 IP 再做过滤判断。只看 URL 字符串、然后做黑名单匹配的做法,遇到重定向、DNS 重绑定、IP 变体格式时基本形同虚设。
7.3 协议白名单比黑名单好用
从扫描器的角度回看,协议白名单是性价比最高的防护手段。只允许http://和https://,禁止跳转到其他协议;限制重定向的次数;对内部服务域名做单独隔离。这套策略虽然不能覆盖所有场景,但能把绝大多数自动化探测挡在门外。
最后说一点个人体会:写 SSRFScanner 的过程中,最大的收获不是代码本身,而是对“请求生命周期”的理解——一个 URL 参数从进入业务系统到最终发起网络请求,中间每一步都可能被误判、被绕过、被拦截。把这个流程想透了,做扫描器和做防护其实是同一件事。建议你在自己的测试项目里,先把一个手工 SSRF case 从入口到回显完整走一遍,再回头来看这个工具的配置和报告,会顺手很多。