
SSRF 这个名字出现在漏洞报告里的时候通常都不是单独一个洞而是一整条内网攻击链的起点。做 Web 安全的朋友应该都有过这种经历业务方报过来一个“远程图片抓取”或者“链接预览”的功能你随手把路径替换成http://127.0.0.1或者http://192.168.1.1响应里居然真的露出了不该出现的内容。这个时候基本可以判定服务端请求伪造——SSRF 跑不掉了。SSRFServer-Side Request Forgery服务端请求伪造的本质是攻击者控制了服务端发起的请求目标让服务器去访问它本不该访问的地址。因为请求是从服务端发出的它天然绕过了防火墙、 NAT 和内网隔离可以直接触碰内网资源。利用得当内网探测、端口扫描、本地文件读取、内网重要服务攻击都能串起来危害远远不止“读个内网页面”这么简单。这篇文章我会从漏洞成因、内网探测、端口扫描利用、绕过手法到修复落地把 SSRF 这条链路完整捋一遍既适合刚入门的安全研究人员参考也能给开发同学提供一份避坑指南。CTF 里 SSRF 是高频率考点如果想把基础打牢CTFHub 技能树的 SSRF 模块是很好的练手场本文很多思路也会沿着这类题目的套路展开。1. 漏洞成因拆解服务端凭什么替攻击者发请求1.1 SSRF 的完整含义与攻击模型先套一个最简单的模型客户端请求你的服务器你的服务器再去请求另一个资源最后把结果返回给客户端。正常情况下这个“另一个资源”是业务预设的比如用户上传图片后服务器去抓取图片做缩略图URL 是用户提供的但服务器会假设这个 URL 指向的是公网图片站。问题就出在这个“用户提供 URL”的动作上。攻击模型可以理解为客户端 → 受控服务器 → 任意目标。如果受控服务器对外发请求时使用的是用户可直接影响的 URL而且没有做严格的协议、域名和 IP 校验那攻击者就让服务器变成了一台任人摆布的“代理”。它不仅能访问公网还能访问内网、本机 loopback、云元数据地址甚至是内网里没有密码保护的服务。简单说服务端本来该扮演一个守门人结果攻击者把它的手伸进了不该伸的房间。很多人会问为什么攻击者不直接访问内网因为内网地址在公网不可路由边界防火墙也会拦截入站流量。但服务端发起的是出站请求防火墙通常放行出站流量这就形成了一个天然的通道。所以 SSRF 的关键不在“发起请求”这件事本身而在“没有边界、没有校验”地信任用户输入。1.2 三类典型代码场景通读大量案例后我发现SSRF 几乎都长在下面这几类业务代码里。第一类是远程资源抓取类。最常见的就是图片缩略图、文件预览、视频封面抓取。后端拿到用户传的 URL用 HTTP 客户端拉取内容。比如 PHP 里常见的写法?php $url $_GET[url]; $data file_get_contents($url); echo $data;这段代码对任意 URL 都照单全收攻击者传file:///etc/passwd就可能读到系统文件传http://127.0.0.1:6379就可能探测本机端口。第二类是链接预览 / 分享卡片类。业务方会做一个“发送链接时自动抓取标题和描述”的功能典型实现是后端用爬虫去访问目标页面解析 OG 标签。这种场景的问题在于URL 是聊天消息里解析出来的攻击者可以发一条消息里面放一个内网地址后端就乖乖去访问了。这类功能还经常触发 URL 重定向更容易被组合利用。第三类是 PDF 生成、Webhook 回调、导入 URL 数据类。有个经典案例服务器用wkhtmltopdf把用户传入的网页转成 PDF而wkhtmltopdf在渲染页面时可以请求页面内的资源。攻击者构造一个 HTML 页面里面写iframe srcfile:///etc/passwd生成的 PDF 里就会包含本地文件内容。另一些导入功能允许用户填 API 地址服务器去拉取数据如果后端同时支持dict://、gopher://等非 HTTP 协议那攻击面会更大。用 Java 写代码的朋友可能熟悉这种场景String apiUrl request.getParameter(apiUrl); URL url new URL(apiUrl); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.connect();URL.openConnection()本身不限制协议和目标地址内网、file、gopher 全都可以被访问。业务代码对用户输入的信任就是漏洞的温床。1.3 危害影响范围SSRF 的危害通常不是单个漏洞能衡量的它更像一个“入口能力”。从攻防视角看一般会带来 5 种直接影响内网存活探测服务器可以访问内网网段通过 HTTP 请求状态码、响应延迟、返回内容判断内网主机的存活情况。端口扫描探测内网权限有限但通过访问不同端口并观察响应差异可以枚举开放服务。本地文件读取支持file://协议时可以读取服务器上的/etc/passwd、配置文件、应用源码等敏感文件。内网应用攻击SSRF 可以作为跳板进一步攻击内网 Redis、MySQL、Elasticsearch 等未授权或弱口令服务甚至直接getshell。云元数据获取在云环境里SSRF 经常会指向云厂商的元数据服务地址如169.254.169.254这类仅限内网访问的地址一旦读取成功可能拿到云主机的临时凭证后果非常严重。这也是为什么很多企业把 SSRF 列为高危漏洞一旦确认就要求业务方当天修。它不是“信息泄露”那么简单而是可能发展到内网沦陷的入口。2. 内网探测先给目标内网画一张地图2.1 探测前的协议可用性判断拿到一个 SSRF 点之后第一件事不是扫端口而是判断目标服务器支持哪些协议。因为不同的目标环境函数实现不一样支持的协议也不一样。比如 PHP 的file_get_contents支持http、https、file、php等协议Java 的HttpURLConnection默认只支持 HTTP 和 HTTPScurl扩展则能支持gopher、dict、ftp等一堆协议。我的探测习惯是先发三个请求做对照快速判断边界http://www.baidu.com确认出网是否正常返回的内容是不是正常页面http://127.0.0.1:80确认能否访问本机端口file:///etc/passwd确认是否支持文件协议读取本地文件。这里有一个关键点不同协议的错误回显差异很大。如果dict://有效你可以通过dict://127.0.0.1:6379/info直接向 Redis 发送命令如果gopher://有效那利用空间会大得多可以构造任意 TCP payload。早期很多 CTF 题目比如 CTFHub 技能树里的 SSRF 基础题就是考察“能不能判断后端支持哪种协议”后面再决定走哪条利用路径。2.2 存活主机探测思路内网网段通常是三块10.0.0.0/8、172.16.0.0/12、192.168.0.0/16本机还有127.0.0.1。如果路由和防火墙配置宽松SSRF 可以直接访问到这些段上的主机。探测存活时我最常用的判断维度有三个响应内容差异正常访问一个不存在的 IP通常会返回连接超时或 DNS 错误。如果某个 IP 返回了 HTTP 状态码或者响应体中包含特定关键字说明该主机存在且对应端口开放。响应时间差异关闭的端口会立刻拒绝连接开放的端口可能需要等待服务响应。在批量扫描时时间差是一个很稳定的判断指标。错误信息差异有些中间件会把内网错误细节返回出来比如“Connection refused”和“Connection timed out”是两种状态这能直接区分 IP 是否存活。举个例子假设存在一个 SSRF 点支持http协议。我用 Burp 的 Intruder 遍历http://192.168.1.1:80到http://192.168.1.255:80通常的做法是GET /?urlhttp://192.168.1.1:80 HTTP/1.1 Host: victim.example.com然后根据响应长度和状态码的差异把存活主机列出来。这个操作本身不难难点在于控制扫描速度和流量避免触发内网安全设备以及避免影响目标业务。安全测试一定是在获得授权的前提下做不要拿这套手法去扫别人家的内网。2.3 无回显时怎么拿信息DNSLog 与时间盲注实际业务里很多 SSRF 并没有回显。服务端访问了目标地址但响应体不会返回给客户端。这时候不能放弃探测还有两个半盲打的思路。半盲打利用 DNSLog。构造一个带有唯一子域的地址比如xxx.dnslog.cn如果服务器真的发起了 DNS 请求DNSLog 平台上就会收到解析记录。这样就算看不到请求内容也能确认“服务器的确能访问该域名”。更精细的探测方式是利用 DNSLog 配合不同端口比如port80.tester.dnslog.cn和port6379.tester.dnslog.cnDNS 解析成功只能说明域名能解析不能直接证明端口开放还需要配合其他手段。盲打利用时间差。访问一个开放端口和关闭端口连接建立时间会有差异。如果服务端请求支持超时配置开放端口往往响应更快关闭端口则立刻失败。但这里有个前提必须能观测到服务端的处理时间差异。通常我会用响应返回的时间戳做粗粒度判断比如访问http://10.0.0.1:6379比访问http://10.0.0.1:9999慢了几百毫秒极有可能说明 6379 端口有服务在监听并产生了超时等待。对无回显场景要把思路从“看响应”切换到“看副作用”。SSRF 的妙处在于它总能留下一些可观测的痕迹DNS 记录、时间差、错误日志、数据库访问记录甚至是目标内网服务本身的报警。这些痕迹只要出现一条就够我们判断内网状态了。3. 端口扫描利用把 SSRF 变成内网端口探测工具3.1 原理协议栈行为差异很多人觉得端口扫描必须用 Nmap 这类工具但实际上只要是能向指定地址发起连接的端点就都能做端口扫描。SSRF 的端口扫描原理和网络层扫描不太一样它走的是应用层连接不同端口返回的“行为特征”不同。TCP 连接本身有三个典型结果连接成功目标端口有服务在监听TCP 三次握手顺利完成。连接被拒绝目标主机在线但端口没有服务监听内核会返回 RST 包表现为“Connection refused”。连接超时目标主机不可达或者防火墙丢弃了数据包表现为“Connection timed out”。这三个结果的微妙差异正好可以通过 SSRF 的返回信息区分出来。比如http://192.168.1.10:22可能返回 SSH banner 或者 400 错误而http://192.168.1.10:99直接报“Connection refused”那就能推断 22 端口开放、99 端口关闭。3.2 不同协议判断端口开放状态HTTP 协议做端口扫描时有几种典型的判断方式。看响应头。访问一个开放 HTTP 服务的端口返回的内容里通常带有Server头、Date头等 HTTP 特征即使服务不是 HTTP 协议因为 HTTP 客户端发送了 GET 请求很多服务会返回一段错误消息比如 Redis 返回-ERR unknown command GETMySQL 则可能直接关闭连接。看状态码和长度。如果访问http://192.168.1.10:8080返回 200 且页面内容长度 5000 字节而访问http://192.168.1.10:9090返回空响应那 8080 上大概率有 Web 服务。构造请求时我一般会用类似下面的格式# 假设存在一个可回显的 SSRF 接口 curl -i http://victim.example.com/ssrf.php?urlhttp://192.168.1.10:6379如果返回内容里有-ERR开头的字符串那基本能确定 6379 是 Redis 服务。当然这个判断方式要求 SSRF 接口能带回显如果无回显就需要结合 DNSLog、时间差等方式组合判断。非 HTTP 协议如dict://和gopher://的威力更大。dict://协议本身就是一个可以发送命令的协议向dict://192.168.1.10:6379/info发出的请求会自动转换成 Redis 的INFO命令结果会原样返回。这种特性在 Redis 未授权场景里特别好用能直接确认目标 Redis 是否允许无密码访问。gopher://则更进一步可以把任意字节流发送给指定端口的 TCP 服务这是利用 SSRF 攻击内网应用的核心通道。3.3 配合 file 协议读取本地文件端口扫描之外file://协议是 SSRF 链路里成本最低、见效最快的一环。一个支持文件协议的 SSRF 点可以直接暴露服务器上的敏感文件这是后续提权的关键信息源。在 Linux 服务器上我通常会先读这几个文件/etc/passwd判断系统上有哪些用户以及服务运行账号/etc/hosts查看内网主机的域名解析关系经常能发现内部域名/proc/self/environ查看当前进程的环境变量里面可能有数据库密码、云凭证/proc/net/arp查看 ARP 缓存能直接看到同网段存活的设备的 IP 和 MAC 地址/etc/nginx/nginx.conf或/etc/apache2/apache2.conf了解 Web 服务配置寻找其他虚拟主机或内网服务。构造方式很简单GET /?urlfile:///etc/passwd HTTP/1.1 Host: victim.example.com如果接口有回显root:x:0:0:root:/root:/bin/bash这类内容会直接出现在响应里。如果无回显也可以配合文件目录探测和报错信息来推断。file://这个协议不需要出网不依赖内网连通性只要目标操作系统上文件存在请求就能成功所以它的利用门槛非常低。3.4 深入利用内网服务攻击SSRF 的最终目标往往不只是扫端口、读文件而是进入内网后的横向攻击。最常见的组合拳是SSRF → 内网 Redis 未授权 → 写计划任务 / SSH 公钥 → getshell。这套利用能成立的前提是SSRF 支持gopher://或dict://协议内网 Redis 配置了bind 0.0.0.0且没有设置密码Redis 运行账户对目标写入目录有权限。使用gopher://构造 Redis 命令时需要注意 TCP 会话中的每一行都必须以\r\n结尾URL 中的特殊字符还要做编码。比如用gopher://192.168.1.20:6379/_info发送一条INFO命令服务器收到后如果能返回 Redis 版本和配置信息就说明未授权是成立的。接下来就可以用CONFIG SET dir结合SET命令写 payload或者用SLAVEOF做主从复制攻击。值得注意的是这类攻击对执行环境非常敏感不同中间件、不同 Redis 版本、不同权限配置利用条件都会有差异。我不建议大家在真实无授权的系统上尝试更推荐在内网靶场或者 CTF 环境里练手CTFHub 技能树的 SSRF 模块就专门准备了这类递进式题目从基础探测一直做到内网纵深利用。4. 校验绕过与常见缺陷代码已经做了防护怎么还会被打穿4.1 IP 地址变形与域名解析绕过很多开发在做防护时采用的是“黑名单”思路只禁止127.0.0.1、localhost和10.x、192.168.x这些常见内网段。这种策略看着简单实际上漏洞百出因为 IP 地址的表达形式远不止一种。拿127.0.0.1举例它可以表示为十进制整数2130706433八进制写法0177.0.0.1十六进制写法0x7f000001IPv6 写法[::1]带模糊地址的写法http://127.1、http://0、http://0177.0.0.1。很多后端在 URL 解析时并不统一使用inet_pton或者INET_ATON这类函数来处理地址而是直接拿字符串匹配。黑名单里写了127.0.0.1但没写2130706433就很容易被绕过去。通配域名也是一个高频绕过点。像localtest.me会解析到127.0.0.1nip.io和sslip.io支持把 IP 直接写在域名里比如10.0.0.1.nip.io也会被解析到10.0.0.1。如果后端只校验域名后缀比如“允许.com结尾的域名”那攻击者就可以注册一个域名解析到内网 IP或者用这些通配域名直接绕过。4.2 URL 解析差异与重定向绕过URL 解析差异是 Web 安全里非常好用的一个工具。不同语言、不同库对 URL 的解析规则并不完全一致攻击者可以利用解析差异让“后端以为的目标”和“实际访问的目标”不一致。典型的几个例子符号http://example.com127.0.0.1在 RFC 3986 语义里example.com只是用户名真正的主机是127.0.0.1。如果后端只判断了example.com就会放行。#锚点http://127.0.0.1#example.com在有些解析器里把example.com当成 fragment不参与网络连接。\反斜杠http://127.0.0.1\example.com在部分浏览器和解析库中\会被当成分隔符处理。URL 编码http://127.0.0.1%2fexample.com可能在编码后被重新解析成http://127.0.0.1/example.com。重定向绕过则是另一种思路后端校验了第一次传入的 URL但只要目标地址返回 302/301 跳转请求就会跟随跳转到新的地址而新地址没有经过校验。很多 SSRF 防护组件在实现时忘了处理重定向或者只处理单次跳转导致攻击者用一次“合法跳转”绕过整个白名单。应对这类问题需要在重定向后重新对最终地址做一次校验并且限制最大跳转次数。4.3 CTF 场景里的经典套路如果是初学者想快速掌握 SSRF 的利用和绕过CTFHub 技能树的 SSRF 模块是一个很好的训练场。它的题目设计基本覆盖了这个漏洞最常见的考点基础无回显要求探测端口存活状态考察的是利用时间差判断端口文件协议读取要求通过file://读取/flag考察的是协议兼容性判断URL 绕过要求绕过域名或 IP 的限制考察的是 IP 变形和 URL 解析差异内网访问要求访问一个特定的内网 IP 和端口考察的是网段扫描和存活判断Gopher 协议利用要求通过 gopher 打内网服务的未授权漏洞考察的是二进制协议构造和编码转换能力。做题的时候你会发现很多绕过技巧之间是可以组合的。比如先用0x7f000001绕过 IP 黑名单再用符号绕过域名白名单最后用dict://探测 Redis。把 CTFHub 这套题打完你基本能建立一条完整的“SSRF 发现 → 利用 → 绕过 → 深入攻击”思维链。职业安全测试中遇到的大部分 SSRF它的利用路径不会超出这个框架太多。5. 排查实录与修复落地从发现到“让这个洞彻底失效”5.1 实战中常见的四个排查难点在真实的渗透测试和应急响应里SSRF 并不是总能被快速确认下面几个问题几乎每次都会遇到。现象常见原因排查方法请求发出去没有响应目标地址不可达或服务端配置了短超时用http://www.baidu.com对照测试确认 SSRF 点本身是否可用返回内容一模一样看不出差异响应被服务端统一包装或者后端做了 HTTP 状态码替换对比响应长度和延迟必要时切换dict://等协议观察原始返回黑名单限制了常见内网段拦截逻辑只匹配了字符串尝试 IP 变形、IPv6、通配域名等方式绕过有防护组件但扫描还是成功防护组件未处理重定向或域名解析后的二次校验检查重定向后的最终 URL 校验逻辑以及 DNS 缓存策略还有一个我踩过很多次的坑只测http://和file://忽略了dict://和gopher://。很多开发在做端口扫描测试时能通过黑名单但如果后端的请求客户端底层是curl这类多协议库直接支持dict://或gopher://就会立刻打开新一批攻击面。排查的时候一定要把协议覆盖面测出来而不是测完 HTTP 就收工。5.2 防御加固清单修复 SSRF 的正确姿势是“自上而下”做多层防御而不是只堵一两个黑名单。这里把我这几年沉淀下来的加固清单分享出来。第一层请求入口校验。凡是用户能传入 URL 的地方都要做协议白名单只允许http和https禁止file、gopher、dict等协议。不要试图用黑名单去禁止危险协议因为后面容易出现各种解析差异问题。第二层域名与 IP 校验。对 URL 的 host 做解析拿到最终 IP 后要确认这个 IP 不是内网地址、不是保留地址、不是本机地址也不是云元数据服务地址。解析后校验这个动作务必要在这次请求真正发起之前执行不能只校验用户传入的字符串。同时要禁用或严格限制重定向跟随如果业务确实需要重定向要在每次跳转后重新做 IP 校验。第三层出网流量隔离。该限制的不是入站流量而是应用服务器的出站流量。应用服务器如果需要访问公网最好通过专门的代理出口并且用防火墙限制它只能访问公网白名单域名访问内网资源的请求统一走独立的内部网关不要让普通业务服务器直接触达内网。这样即使代码被绕过攻击者拿到的也只是有限的出网通道。第四层云元数据防护。在云环境里务必确认云服务器元数据服务启用了 IMDSv2 等增强认证机制并且对能访问元数据服务的进程做最小权限控制。不要等 SSRF 被利用后才发现云凭证已经泄露。第五层统一防护组件。大型项目里与其让每个业务方自己实现 URL 安全校验不如抽象出一个统一的 SSRF 防护库封装 URL 解析、IP 校验、协议限制、重定向处理等逻辑所有需要发起外联请求的功能强制走这个库。这样能把安全能力集中管控也方便更新和审计。5.3 验证修复是否到位修复做完不等于闭环验证环节非常关键。我一般会准备一组测试用例执行完还要看安全团队是否能在日志中发现扫描行为。用http://127.0.0.1访问本机应该被拒绝用http://0x7f000001访问本机应该被拒绝用http://192.168.0.1访问内网应该被拒绝用file:///etc/passwd读取文件应该被拒绝用http://localtest.me访问本机应该被拒绝用http://example.com重定向到http://10.0.0.1应该被拒绝正常公网图片地址应该能被正常抓取。如果这一组用例全部符合预期才可以说这个 SSRF 点基本封堵住了。还要提醒一句安全测试过程中产生的扫描流量和请求日志务必留给业务方和安全团队留痕方便后续溯源和告警改进。最后分享一点个人体会。SSRF 表面上看是一个“请求伪造”漏洞但真正决定它危害等级的是“出口距离”。出口离本机越近能读文件、打本地服务出口离内网越近能探测内网、打核心资产出口离云平台越近甚至可能直接拿到云凭证。这也是为什么我一直建议开发和运维在做架构设计时就把出站流量管控当成安全基础设施的一部分而不是等出了漏洞再去加过滤规则。内网被 SSRF 撕开一个口子之后修复成本远远高于一开始就把边界切干净。希望这篇内容对其他安全研究者、开发同事有实际帮助少踩几次我踩过的坑。