
1. 理解SSRF一个容易被低估的入口漏洞做安全测试这些年我见过太多团队把精力堆在SQL注入、XSS这些“老熟人”身上却对SSRFServer-Side Request Forgery服务端请求伪造缺乏足够的重视。原因也很现实SSRF在漏洞评级里经常被定为中危而且利用链路往往比直接拿数据要长很多人在日常测试中试了两个payload没反应就直接放弃自然也就体会不到这个漏洞真正的破坏力。但实际情况是SSRF一旦能被完整利用往往能打通一条从外网到内网的通道。相比直接打一个边缘系统这类漏洞更容易突破网络边界。现在微服务架构、云原生环境越来越普及服务端发请求的场景越来越多SSRF的出现频率比很多人想象中高得多。在CTF比赛里SSRF也是高频考点比如CTFHub技能树里专门有SSRF专题从基础过关到进阶利用都有对应的题目设计。这篇文章我会按漏洞成因、探测方法、内网资产发现、端口扫描利用这条链路来拆解最后聊一下防御层面的修复方案。内容同时适合三类人做Web安全测试的从业者、打CTF的选手、以及正在给自己业务做安全自查的开发同学。我会尽量把思路和实操细节都讲透方便你在自己的测试环境里复现。2. 漏洞为什么会存在从代码层看SSRF的成因2.1 服务端请求的本质一个“中转站”被滥用想理解SSRF先想清楚一个场景服务端为什么要发请求很多Web应用存在“中转”需求比如用户提交一个URL服务器去获取这个URL的内容再返回给用户常见于图片抓取、文章转码、网页快照、API代理转发等业务场景。从用户的角度看请求链路是“用户 - 服务器”从服务器的角度看它额外发起了“服务器 - 目标URL”的请求。问题就出在这条“服务器 - 目标URL”的链路上。当服务器没有严格校验目标URL时攻击者就可以让服务器去访问原本不该访问的地址比如内网IP、云元数据地址、本机回环地址。用一句通俗的话说攻击者把服务器变成了自己的代理“借刀”访问内网资源。我把SSRF归结成一句话这是一个传入参数被当作服务端请求目标、且缺乏有效校验导致的“信任边界”失控问题。代码本意是让服务器帮忙取外部资源但攻击者传入的URL让服务器去访问了内部资源信任被滥用就变成了漏洞。2.2 常见触发场景与典型脆弱代码哪些功能容易产生SSRF根据我测试过的项目主要集中在这几类图片处理功能用户传图片URL服务器下载后做缩略图、水印。网页预览/转码传入URL返回对应网页内容或截图。文件导入/导出支持从远端URL导入文件。Webhook配置用户配置一个回调URL服务器主动访问。PDF生成传入URL渲染成PDF。文档处理组件如一些文档预览中间件。这里用一段PHP代码说明最基础的SSRF成因?php // 从用户传入的URL参数获取目标地址 $url $_GET[url]; // 直接请求该URL并把响应返回给用户 $ch curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response curl_exec($ch); curl_close($ch); echo $response; ?这段代码就有明显的SSRF问题但实际业务代码里它往往包着各种外衣比如参数名是file、path、img、redirect、target或者经过加密/编码之后才传到后端需要先识别出真正的请求目标参数。Java侧也有类似场景比如HttpClient直接请求用户传入的URL或者用ImageIO.read(new URL(input))加载远程图片都可能导致 SSRF。Python侧则常见于 Flask/Django 里用requests.get()抓取用户提交的URL。2.3 为什么校验容易做得不到位很多人会问是不是开发同学安全意识不够才导致SSRF其实更多时候是校验逻辑写得“看似有、实则无”。常见的不能说完全没做但可以被绕过的校验方式包括只校验协议头是http://或https://但这本来就在允许范围之内等于没限制。对URL做了域名白名单但存在重定向绕过。做了IP格式黑名单但没考虑IPv6、短域名、进制转换、URL解析差异等绕过手段。使用了不严谨的正则去匹配“内网IP”比如只匹配了192.168开头的地址漏掉了10.、172.16-31、127.等网段。这些场景我都在实际项目里遇到过后面在探测和利用部分我会专门展开讲常见的绕过姿势。3. 漏洞探测如何快速确认SSRF的存在3.1 从功能点反推请求链路拿到一个目标第一步不是急着打payload而是先梳理功能点。我一般会先过一遍流程先看网站有哪些功能可能触发“服务器发起请求”比如上传图片时是否支持URL方式、是否存在导出PDF功能、是否有URL分享预览、管理后台里是否有站点健康检测。确认功能点之后再观察请求包。重点看两个地方一是GET/POST参数里有没有URL、link、src、path、uri、target、domain这类参数名二是响应中是否直接或间接反映请求结果比如返回图片、返回标题、返回页面内容片段等。这里有个小技巧如果响应里能看到目标URL对应页面的内容那几乎可以确定服务端做了代理请求这就是一个切入点。但如果响应只返回成功失败状态也不代表一定没戏可以用时间盲注式的思路让服务器请求一个自己能控制的地址观察请求是否到达。3.2 用可控域名做外带检测我在测试中最常用的第一个payload就是让目标服务器访问我自己的服务器然后在服务器上监听请求。这里用nc监听就可以nc -lvnp 8888然后在目标功能点提交https://example.com/ssrf_test.php?urlhttp://your-server.com:8888/test如果服务器收到请求说明URL参数能控制服务端发出请求SSRF确认存在。这种方法最直接也最适合第一步验证。没有公网服务器的话也可以用一些公开的DNSLog平台提交URL后去平台查看解析记录只要能收到解析请求就说明服务端确实发起了对外请求。不过要注意DNSLog只能看到域名解析情况看不到完整的HTTP响应内容等到后面需要读取数据的时候还是需要一个自己能完全控制的服务器来接收数据。3.3 利用响应差异区分过滤规则在确认“能请求”之后下一步要摸清目标对URL做了哪些过滤。这一步直接决定后续利用方式。我会用一组对比请求来探测过滤规则测试URL预期结果观察点http://your-server.com:8888/plain正常HTTP请求外部服务器是否收到file:///etc/passwd读取本地文件响应内容是否包含文件内容http://127.0.0.1:8080/访问本机端口是否返回不同状态码/响应时间gopher://your-server.com/发起TCP协议请求是否支持gopher等非常规协议http://your-server.com:8888/redirect构造302跳转到内网服务器是否跟随重定向根据每组请求的响应差异可以推断目标后端是否限制了协议、是否过滤了内网地址、是否禁止了特殊端口、是否跟随重定向。比如返回“协议不支持”和返回“连接超时”就是完全不同的两码事前者说明有协议白名单后者说明请求已经发出但目标不可达。这里特别提醒一点判断SSRF是否存在时不要只依赖响应内容。很多后端在请求失败时返回一样的错误页面这时需要用时间差来辅助判断。比如让服务器去请求一个不存在的内网IP如http://10.0.0.254:9999/观察响应时间再对比请求一个真实存在的内网IP如http://10.0.0.1:80/如果时间上出现明显差异说明内网探测有了方向。4. 内网探测把SSRF变成内网的“望远镜”4.1 从回环地址出发试探本机服务SSRF利用的第一步往往是从访问127.0.0.1开始的。为什么先打本机两个原因一是本机服务最容易验证SSRF是否可用比如请求http://127.0.0.1:80、http://127.0.0.1:8080看是否能得到管理后台二是很多内网架构里本机跑着数据库、Redis、消息队列、监控组件等敏感服务这些服务如果监听了本机端口但没有做访问控制SSRF可以直接摸到。我在一次授权测试中遇到过这样的场景目标是一个内容管理系统存在SSRF。系统本身跑在192.168.10.5上本机开了3306端口。通过SSRF访问http://127.0.0.1:3306返回的握手包信息直接暴露了MySQL的版本号。虽然这次没能直接拿到数据库内容但至少确定了内网里有哪些基础组件为后续利用积累了信息。4.2 网段发现与存活IP判断确认能访问本机后下一层就是探测内网网段。怎么判断内网网段有几个思路从业务域名反查IP推测所在网段。比如业务域名解析到10.10.10.5靶机很可能就在10.10.10.0/24网段。观察响应头信息、错误信息中可能泄露的内网IP。利用云环境元数据地址判断比如http://169.254.169.254/latest/meta-data/如果返回内容说明在云环境中。在确认了可能的网段后探测存活主机的方式很简单利用SSRF依次请求目标网段内不同IP的常见端口根据响应差异判断存活情况。这里的关键在于批量操作时的速度控制。SSRF请求是串行的一个URL对应一个请求如果目标网段非常大如10.0.0.0/8不可能一个个手动提交需要写脚本自动化。我常用的方式是写一个Python脚本通过代理工具或直接调用目标接口来批量发请求。import requests # 目标存在SSRF的接口 url http://target.com/fetch # 需要探测的网段和端口 ip_base 192.168.10. port 8080 # 结果保存 alive_hosts [] for i in range(1, 255): target_ip f{ip_base}{i} target_url fhttp://{target_ip}:{port}/ params {url: target_url} try: r requests.get(url, paramsparams, timeout5) # 通过响应长度/状态码/内容判断是否存活 if r.status_code ! 502 and r.text ! : alive_hosts.append(target_ip) print(f[] alive: {target_ip}) except Exception as e: print(f[-] {target_ip} error: {e}) print(alive_hosts)实际测试中用脚本扫描时有两点要非常注意。一是尽量降低请求频率给目标接口加个延迟避免触发WAF或导致目标服务过载二是要确认自己是否获得了授权“扫内网”是一个敏感动作务必在合法授权范围内进行。如果是在CTF练习环境中目标就是设计给你打的可以放心操作。4.3 绕过常见过滤规则的姿势探测内网时最常遇到的就是“IP被过滤”。不少开发同学会用黑名单方式拦截一些常见的内网地址段比如过滤127.0.0.1、10.、192.168.。但绕过方式五花八门这里我列出实际测试中常用的几种十进制/八进制IP表示http://2130706433/等价于http://127.0.0.1/、http://0x7f000001/也指向回环地址。URL解析差异http://127.0.0.1evil.com/中许多解析器会访问127.0.0.1而不是evil.com。有些后端做了域名白名单但没处理好符号就能直接绕过。短链接/跳转服务先用一个允许访问的短链接地址再由服务端跟随302跳转到内网地址很多场景下都能绕过“域名白名单”。IPv6地址有些过滤只匹配IPv4用http://[::1]:8080/就能访问本机。十六进制编码的IPhttp://0x7f.0x0.0x0.0x1/在某些语言里可以被解析为127.0.0.1。我遇到的比较奇怪的一种情况是后端用正则匹配了127.0.0.1但只要把地址写成127.1就能绕过。原因很简单部分网络库会把127.1解析为127.0.0.1但正则没有覆盖这种情况。4.4 云环境中的元数据攻击在云原生环境里SSRF一个特别恶心的利用场景就是访问云元数据服务。在阿里云、腾讯云、AWS等平台元数据服务通常是http://100.100.100.200/latest/meta-data/或者http://169.254.169.254/latest/meta-data/。这类地址如果通过SSRF可以访问到攻击者可能获取到云账号的临时凭证、实例ID、安全组信息等敏感数据甚至进一步接管云资源。之前发生过多次真实事件比如某厂商的文档预览功能因SSRF漏洞导致攻击者通过元数据接口拿到云主机的临时访问凭证最终批量获取对象存储中的文件。因为非常敏感我这里点到为止。核心想表达的是在云环境场景下SSRF的威胁等级会直线上升。如果你在测试时发现目标跑在云上一定要去尝试访问元数据地址不管能不能利用至少要在报告中写清楚风险。5. 端口扫描从SSRF出发摸清内网服务5.1 基于响应差异的端口状态判断SSRF作为端口扫描器的原理说起来很简单让服务器去访问http://内网IP:端口根据响应结果判断端口开闭。但“响应结果”怎么区分不只是“通”和“不通”两种状态实际测试中至少有以下几种区别端口开放且返回HTTP协议内容响应中带HTTP状态码、响应头、页面内容。例如访问http://192.168.1.10:8080/返回了Tomcat默认页面或登录页面。端口开放但非HTTP协议有些服务不是HTTP如MySQL、SSH、Redis。此时SSRF请求可能超时、报错或返回空内容但超时表现和端口关闭不同。MySQL这类服务收到HTTP请求后会立刻关闭连接响应时间非常短而端口关闭时连接直接被拒错误类型也可能不同。端口关闭连接被拒绝响应时间极短通常返回一个错误信息或者空白页面。防火墙DROP请求超时且响应为空和“端口开放但非HTTP”容易混淆需要结合多个端口综合判断。表格整理一下对比探测结果端口状态判断响应表现特征返回HTTP协议内容端口开放Web服务含状态码、HTML、响应头连接后立刻中断端口开放非HTTP服务响应为空或异常请求时间短连接被拒绝端口关闭响应快速失败带错误信息请求超时防火墙拦截或主机不可达耗时很长响应为空利用这些差异一次SSRF请求就能判断一个端口的大致状态。结合自动化脚本可以批量扫描内网主机的端口分布摸清内网的服务架构。5.2 基于时间差异的盲扫技巧如果目标场景比较苛刻比如响应内容不可见、只能通过时间差判断但也没有太大影响。核心原理是同一网段内存活主机对连接请求的响应时间和不存在的主机、被防火墙拦截的主机有明显区别。具体操作上可以让SSRF端依次请求同网段不同IP的相同端口记录每次请求的耗时。活着且端口开放的主机响应快端口关闭的主机拒绝快但错误类型不同IP不存在或防火墙拦截的主机则会等较久。这种方式虽然精度不如直接看响应内容但在接口不返回请求结果、只能通过时间盲区判断时非常实用。我一般在目标确实“抠门”的情况下才用这个方案因为它效率不高且对网络环境敏感需要做多组对照减少误差。5.3 利用Gopher协议扩展利用面探测完内网端口之后很多人的利用就卡住了虽然发现内网有Redis、有MySQL但通过HTTP协议只能确认端口存在拿不到更多的数据。这时候Gopher协议就是SSRF利用中的一个杀手锏。Gopher协议允许客户端构造原始的TCP数据包这意味着只要内网某个服务是纯TCP协议且没有做认证或者存在其他可利用点就可以通过SSRF拼接一个完整的请求来触达它。CTFHub的SSRF技能树里有一类关卡就是专门练习Gopher协议的。比如利用Gopher打内网Redis可以构造Redis命令往目标Web目录写一句话木马或者写入SSH公钥等。我练习过之后对这种利用链的完整流程理解更透彻了。为了不过度展开我只提一个基本思路真正的细节需要读者在自己搭建的靶场里慢慢调确认目标开启Gopher协议先提交gopher://your-server.com:8888/test看你自己的服务器是否收到TCP连接。构造Redis命令数据把要执行的Redis命令拼接成字节流。对字节流做URL编码后作为Gopher协议地址传入SSRF。注意部分语言对Gopher支持不好比如PHP的curl就需要确认编译时是否开启了Gopher支持。实际工作中Gopher协议利用的成功率受限于后端语言和curl编译选项我遇到过几次客因为curl不支持gopher而利用失败的场景。不过在CTF场景中出题人往往已经预留了协议支持所以练习时不用太担心。5.4 内网服务指纹识别与资产测绘端口扫描的最终目的是摸清内网资产识别出有价值的服务。通过SSRF拿到端口开放情况后下一步就是识别“哪个服务值得打”。常见的内网高价值服务包括未授权访问的Redis6379未授权访问的MongoDB27017未授权访问的Elasticsearch9200这个端口通过HTTP即可获取信息SSRF探测直接有效。Docker远程API2375Spring Boot Actuator环境常见于内网微服务消息队列管理后台如RabbitMQ的15672数据库服务MySQL 3306、PostgreSQL 5432、SQL Server 1433通过SSRF访问这些端口后根据返回内容可以判断版本信息、是否存在未授权访问。比如访问http://内网IP:9200/如果返回了集群名称和版本号那大概率可以继续尝试直接读取索引数据。类似的访问http://内网IP:2375/version如果返回Docker版本信息就说明Docker API可能未授权。我做一个资产测绘时一般会用这样一个信息表来记录内网IP端口服务指纹可利用性评估192.168.10.53306MySQL 5.7.30可确认版本尝试弱口令192.168.10.56379Redis 4.0.9尝试未授权访问192.168.10.89200Elasticsearch 7.6存在未授权访问风险有了这张表后续的利用路径基本就清晰了。为什么这一步重要因为内网渗透最怕的就是瞎打清晰的资产表能帮助你快速定位薄弱点而不是东一下西一下做无用功。6. 防御与修复上线前必须做的事6.1 代码层的防护思路了解完SSRF的利用链路防御思路就非常清晰了。最核心的一条对服务端发起的请求目标做严格白名单校验。不是黑名单是白名单。白名单校验分这么几层协议限制只允许http://和https://禁止file://、gopher://、dict://等非常规协议。域名/IP限制维护一份允许请求的域名白名单或IP白名单不在白名单内的一律拒绝。端口限制默认只允许80、443对其他端口一律拒绝。这个看起来简单但很有效可以挡住大量内网服务探测。重定向限制把跟随重定向功能关闭或重定向后重新校验URL是否合法。在Java的HttpClient实现里关闭重定向很简单PHP的curl则需要设置CURLOPT_FOLLOWLOCATION为false。如果业务确实需要跟随重定向那每跳一次都必须重新做白名单校验不能跳完就放行。6.2 网络层的隔离手段代码层的白名单再严密也会有被绕过的一天。所以网络层的纵深防御必须有内网隔离应用服务器和数据库、Redis等服务必须做网络隔离应用服务器不能随意访问内网所有主机。出口访问控制应用服务器的出网方向要做限制只允许访问所需的外部地址。云环境防护云上资源通过安全组限制元数据服务的访问可以在系统层用防火墙规则阻止容器访问169.254.169.254、100.100.100.200等元数据地址。这些网络层的手段可能导致业务部署麻烦一些但关键时刻真的能刹住车。我见过很多代码层校验不完善的应用如果不是内网做了严格隔离SSRF造成的危害会大得多。6.3 DNS Rebinding攻击的额外提醒DNS RebindingDNS重绑定是SSRF防护中经常被忽略的一点。攻击流程大概是这样攻击者注册一个域名第一次解析时返回公网IP通过白名单校验等绕过校验后第二次解析返回内网IP如192.168.1.1此时服务端实际请求的是内网地址。防这一类攻击除了白名单校验外还需要在代码里对最终解析出的IP做一次合法性检查。也就是说拿到域名后先解析出IP确认IP不在内网网段再发起请求。这个逻辑看起来简单但因为要保证“解析校验”和“实际请求”使用的是同一个IP很多实现里都有时间窗口上的差异处理起来有一定门槛。如果后端是PHP一个简单的思路是先dns_get_record()解析域名比对IP合法性再使用socket连接指定的IP和Host头来请求。但这种方式改造成本高对大部分团队来说优先做好前面说的白名单和网络隔离就能挡住90%的SSRF攻击了。7. 常见问题与排查技巧实录7.1 为什么添加了过滤但SSRF还是存在这是我在代码审计中最常见的疑问。开发同学说“我明明过滤了127.0.0.1和192.168”但测试时利用http://127.1/就绕过了。问题出在“黑名单正则”永远追不上解析器的灵活行为。刚才提到的多种IP表示方式只是冰山一角。URL解析层面还有更多问题http://2130706433、http://0x7f000001、http://0177.0.0.1都是127.0.0.1的不同写法。想靠正则去穷举这些变体基本是条死路。合理的做法是在后端代码里直接获取解析后的目标IP再用编程语言里的函数判断这个IP是否属于内网网段不要自己写字符串匹配。举个例子在Python里可以用ipaddress模块来判断import ipaddress def is_internal_ip(ip_str): ip ipaddress.ip_address(ip_str) return ip.is_private or ip.is_loopback or ip.is_link_local这里为什么要特别提这一点因为很多人写过滤逻辑时习惯用str.contains或正则匹配误以为“包含192.168就算拦住了”但后面马上就会遇到各种绕过。用库函数解析IP再做判断才是正确姿势。7.2 如何避免扫描被拦截用SSRF做内网探测时最怕的不是漏洞不存在而是扫到一半被WAF拦截或者账号被封。我这里有一个心得控制速率像一个“正常用户”而不是“扫描器”。具体来说每次请求之间加至少200毫秒的延迟不要用并发请求去测同一台目标每个目标IP只测必要端口不要对全端口段暴扫最重要的是先梳理清楚目标内网的网段范围再动手而不是盲目扫所有网段。还有一点如果你的请求都是同一台服务器发起的后端可能会基于源IP做频率限制这时可以尝试调整X-Forwarded-For等头部观察后端是否会根据该头部做限制。但这属于“绕过风控”的技术细节在授权测试中才可以使用日常自行搭建靶场练习时不需要管这些。7.3 不同语言/环境下的SSRF差异不同后端语言对URL的处理和请求行为不一致这直接影响漏洞利用的难度和方式。语言/环境常见请求函数特点PHPcurl_exec()、file_get_contents()curl功能强大支持gopher/dict等协议时风险高JavaHttpURLConnection、HttpClient默认支持常用协议重定向处理较为严格但版本差异大Pythonrequests、urllib支持的协议有限如果引用了requests且未做防护仍可利用Node.jsaxios、http模块默认只支持HTTP/HTTPS但需要关注DNS解析层面的问题在CTF中PHP的SSRF题目占多数就是因为curl的协议支持非常丰富出题人可以根据知识点设计更多关卡。而在真实业务中Java和Go的服务更常见这些语言默认支持的协议有限利用面不如PHP但探测内网、访问元数据接口依然可行。7.4 测试环境搭建建议如果你看完文章想动手练习不需要急着找目标完全可以自己搭一个靶场。我的建议是这样的随便找个云服务器装一个简单的Web应用自己写一个带SSRF漏洞的接口可以参考本文第2.2节的PHP代码。再开一台内网机器跑一个Redis或MySQL作为“内网目标”。通过云服务器上的SSRF去探测另一台机器的端口体验完整的利用链路。这样一套环境十分钟就能搞定但能帮你把本文提到的探测、扫描、指纹识别链路都跑通。如果不想自己搭也可以去CTFHub这类在线技能树平台刷SSRF关卡题目设计更系统利用Gopher之类的特殊协议时也不需要自己搭环境。最后分享一点我的体会SSRF这个漏洞刚接触时觉得原理很简单但真正深入之后会发现很多细节值得琢磨。比如URL解析的差异、不同协议的支持情况、内网探测时的响应判断每一个环节都可能踩坑。我记得第一次用SSRF探测内网时写了个脚本扫了整个C段结果因为频率太快把自己的云服务器IP封了从那以后就养成了控制速率的习惯。给新手的建议是不要一上来就追求打Redis、打MySQL这种高端操作先把“探测内网端口”这一步跑稳。能熟练判断端口开闭、区分HTTP和非HTTP服务之后再往Gopher协议等方向深入这样进阶会顺畅很多。等你在CTF里把SSRF的利用链路完整打透一遍再回头看真实业务很多以前觉得不痛不痒的点会突然变得清晰起来。