资讯动态

SSRF漏洞实战:从Gopher协议到无回显利用与防御

发布时间:2026/10/22 1:42:26 来源:尧图企业网站定制
1. 从漏洞第一现场说起SSRF 的本质拆解先说一个我常用的比喻如果普通 Web 漏洞是“你被骗着交了密码”那 SSRFServer-Side Request Forgery服务端请求伪造就是“你被骗着替别人跑腿送信”。攻击者并不直接接触内网而是通过一个“会出外勤”的服务端功能让它去请求攻击者指定的地址。而这个功能恰恰是很多系统里最不起眼、但也最容易被忽略的部分比如 URL 参数、图片裁剪、网页截图、PDF 导出、Webhook 回调、页面转码等等。在我接触过的项目里SSRF 之所以难修、难挖是因为它不像 SQL 注入那样有一个相对固定的“注入点”。它是一个逻辑型漏洞隐藏在“服务端有没有按你的意思去发请求”这条链路里。它可能出现在用户提交一个网址服务器去抓取并生成封面用户提交一个 IP服务器去查询“主机状态”用户提交一段 SVG/HTML服务器去无头浏览器渲染用户提交一个回调地址服务器用来接收事件通知。判断一个点是否可能存在 SSRF核心就一句话服务端是否接收任何一个“会被当作地址/目标”的输入并主动发起了网络请求。如果答案是“是”那这就是需要重点排查的候选位。而深度利用方面SSRF 的价值也分层级。最基础的是读取内网未授权接口、访问云元数据服务进一步则是配合 Gopher 这类协议打 Redis、打 MySQL、打应用层未授权服务再往上如果响应不可见还可以通过各种带外信号把“看不到”变成“看得到”。下面我逐一拆解最后放一个完整的实操靶场流程。1.1 SSRF 为什么危险服务端替你发请求很多刚上手的朋友会有一个疑问“不就是让服务器访问了一个地址吗有什么不能访问的”问题在于服务器和你不一样它在网络里的位置很特别。第一服务端是内网的一部分。它可能可以访问10.x.x.x、172.16.x.x这类内网网段。而这些地址对于外部攻击者来说往往不可达。如果服务端没有做严格的访问限制你就能用“它替你请求”的方式去看内网里的东西。第二服务端可能持有本地云角色凭证。现代云环境下几乎每个计算实例都有一个“元数据服务”一般在类似169.254.169.254的地址。如果 SSRF 能打出这个地址就可以拿到临时凭证。这就不是“看一眼内网”那么简单了而是拿到一个能够在云环境内横向移动的身份。第三服务端可以到达的不仅仅有 HTTP 服务。Gopher、Dict、FTP、LDAP 这类协议注册的端口也可能被命中。很多内网数据库、缓存、消息队列默认没有强认证一旦被 SSRF 组合打穿后果远大于一个普通信息泄露。这也是为什么很多企业把 SSRF 视为高危甚至严重级别漏洞。它不只是一个“请求跳转”而是攻击者拿到了“服务端的网络身份”作为代理。在授权测试中遇到 SSRF 时我的习惯是先判断“我能让服务器往哪打”再判断“我能拿到什么响应”最后才考虑“怎么利用”。1.2 三种典型形态有回显、半回显、无回显SSRF 的利用方式很大程度取决于你能否看到响应内容。我在实际测试时会把情况分成三类有回显形态服务器把你请求的响应内容直接渲染在页面上。比如网页截图服务截取目标页面或图片代理把目标图片返回给用户。这个形态最好用可以直接读取内网页面的内容。但要注意所谓“回显”也不是百分百完整有各种编码、转义问题。半回显形态服务器不把完整响应返回但会暴露一些信息比如报错里包含响应状态码、响应头或者某个业务逻辑会因响应内容不同而出现差异。这种形态需要仔细“审问”服务器通过错误信息、状态码差异来一点点还原内部情况。无回显形态服务器发出请求但你没有看到任何结果。端口探测的“时间差”是你为数不多的信号。实际上无回显是渗透测试中非常常见的情况很多业务系统在抓取 URL 之后只返回一个 OK或者什么都不返回。认清形态很重要因为它决定了后续的打法。我见过不少测试者拿一个无回显的 SSRF 去验证 Redis 攻击结果发现半天没有回包就以为失败了实际上载荷可能已经执行成功只是没有回显而已。后面我会专门讲无回显形态下怎么建立“盲信号通道”。1.3 攻击面在哪里从参数到逻辑的排查路径测试 SSRF 不是漫无目的地随便翻参数。我一般按两条路径去排查。第一条是找“URL 型参数”。通篇扫描所有输入参数凡是可以填一个网址、域名、IP、端口的参数都可能是候选。比如?url、?src、?target、?callback、?redirect、?page等等。即使是 JSON 包里嵌套的子字段也可能被服务端拿去请求所以不能只看查询字符串。第二条是找“文件处理型逻辑”。比如头像上传时如果允许传“图片 URL”服务器先下载再处理那就是一个很典型的 SSRF 位。还有 PDF 生成器如果可以把一个外部链接“导入”成页面那服务端就会去访问那个链接。这些功能在功能测试时往往不容易被注意到反而是安全测试的高优先级目标。除此之外301/302 跳转也是一个容易被忽略的扩展点。很多开发者只校验了第一个请求的域名但实际上服务端可能跟着跳转而被跳转到的地址完全不受最初校验的限制。我在实际测试中遇到过只校验www.example.com的情况但如果让服务器访问www.example.com它跳转到了内网或云元数据地址校验就被绕过了。这属于“校验了入口却没有校验出口”的经典缺陷。2. Gopher 伪协议把攻击面拉满的协议技巧如果 SSRF 只能发 HTTP 请求那它的攻击面其实是受限的。因为 HTTP 请求需要服务端有对应的应用逻辑去处理否则很难扩大战果。但 Gopher 协议的存在把这件事的层次拉高了不少。这也是很多新手在入门 SSRF 时觉得最“卡”的一个知识点所以有必要单独讲清楚。2.1 为什么偏偏是 GopherGopher 是一个非常古老的协议比 HTTP 还要早。它的核心特点非常“朴素”Gopher 客户端会把 URL 里 path 部分对应的数据原封不动地发送给目标服务器。注意是原始的字节流没有任何一层封装、转义或者协议内转换。正因为有这个“透明传输”特性Gopher 在 SSRF 利用中的价值变成了你可以用它“伪造”任意基于 TCP 的文本协议请求。HTTP 只是其中之一Redis 的 RESP 命令、MySQL 的握手包、Memcached 的文本协议、SMTP 的命令只要目标端口开放并且协议是文本型的理论上都可以通过 Gopher 去“代打”。我举一个直观的例子。Redis 默认监听 6379很多内网环境里它没有设置密码或者只绑定内网网卡。攻击者的外部主机无法直接访问但是 SSRF 所在的服务端可以。那么通过 Gopher 构造一个 Redis 命令序列就能让服务端去“执行”这些命令。最常见的是往目标网站目录写入一个 webshell或者把公钥写入目标主机的authorized_keys从而直接获得服务器权限。这个链路的中间人就是 Gopher 协议。所以 Gopher 的意义不是“多了一种协议可打”而是它把“SSRF 只能打 HTTP 功能”扩展到了“SSRF 可以打几乎任何 TCP 服务”。攻击面一下子开阔了很多这在授权测试中尤其值得关注。2.2 构造 Gopher 载荷的完整思路既然 Gopher 的核心是“原样发送”那构造载荷的关键就是让发送出去的字节流符合目标服务协议的格式。我这里以打 Redis 为例给出完整的构造思路。Redis 是一个文本协议客户端和服务器之间通过*和$组成的数组格式来传递命令。比如SET mykey myvalue这个命令在 RESP 协议里长这样*3 $3 SET $5 mykey $7 myvalue每行末尾需要用\r\n分隔。如果我要让 Gopher 替我去发这段数据就需要把它套进 Gopher URL 的格式里。标准格式是gopher://目标IP:端口/_后面跟着要发送的数据_是一个常见的分隔符实际发送时它会被替换成空字符串后面的数据会原样发送给目标。然后需要把所有控制字符做 URL 编码SET mykey myvalue经过切分和编码之后大概是gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aSET%0d%0a$5%0d%0amykey%0d%0a$7%0d%0amyvalue%0d%0a注意%0d%0a就是\r\n的 URL 编码。这里有个非常容易踩坑的点HTTP URL 本身也会对特殊字符做编码如果整条 Gopher URL 作为参数传给 SSRF 时就容易出现二次编码问题。也就是说目标代码里如果用了urlparse或类似组件处理参数你精心构造的%0d%0a可能会被再编码一次变成%250d%250a那 Redis 收到的就不是换行而是一串无意义的字符。我建议的实操顺序是先在本地用模拟环境把 Gopher 载荷测试通过再放进 SSRF 参数里做整体调试。具体做法后面实操环节我会展开。这里先把构造思路说清楚协议原文 → 按协议格式换行 → URL 编码 → 拼接 Gopher URL。每一步都要知道自己在做什么而不是只会用现成脚本。2.3 协议白名单下的取舍与变通不少系统会做协议白名单只允许http和https那就意味着直接用 Gopher 这条路被封死了。但这不等于 SSRF 束手无策我总结了几种常见的变通思路。一种是利用 HTTP 的 30x 跳转。有些系统的协议白名单只检查初始请求的 scheme并没有阻止后续的跳转。如果目标服务端跟随 301/302那你完全可以把一个允许的 HTTP 网址做成跳转器让它最终跳到gopher://地址。我见过一些实现里对跳转次数和目标协议没有做二次限制这样就轻松绕过了入口校验。另一种是正确的协议构造。有些服务端会用“按端口推断协议”的方式做白名单比如只允许 80 和 443 端口。但 Redis 默认是 6379 端口那可以把 Redis 的端口手动改成 80 或者 443不少自建环境里真的会这么配置。还有一个思路是使用dict://协议它能够向指定端口发送“空格分隔的命令”在探测一些简单协议的端口时非常好用缺点是它不适合复杂命令因为协议层本身对数据格式有封装不能完全替代 Gopher。另外一种在较新环境中常见的手法是借助有漏洞的 HTTP 层服务来“中转”。有些内网应用本身存在“二次 SSRF”它会把收到的某个参数当作下一个请求目标。这时从入口进入后通过这个应用再跳出去就形成了链式利用。整个链路看起来比较曲折但在真实环境中并不罕见。3. 无回显利用看不见响应时怎么打我觉得这一节是最有实操价值的部分。很多人学 SSRF 时看的都是“有回显直接读文件”的爽文案例一遇到无回显就不知道怎么办了。其实无回显并不是死胡同而是要换一套信息提取思路把“看响应”变为“看副作用”。核心原则是想办法让服务器在发起请求之后留下某种你能观察到的“痕迹”。下面几种方式是我实测中反复用到的。3.1 带外判断通道DNS 日志与外部请求最常见的无回显判断手段是“带外通道”英文一般叫 OOBOut-of-Band。原理非常简单让 SSRF 发起一个请求到一个你能够控制日志的外部域名然后你观察这个域名是否收到了 DNS 解析请求或 HTTP 请求。比如我有一台自己控制的服务器配置好某个域名oob.example的子域名解析记录。只要 SSRF 发出请求访问probe1.oob.example我的 DNS 服务器上就会留下一条解析记录。不管响应是否回显这一条记录就足以告诉我服务端确实发出了请求而且请求携带的路径、参数、所在 IP 全都暴露在 DNS 日志里。无回显场景下判断网络连通性我习惯先打一次 DNS 解析再打一次 HTTP 请求如果只收到 DNS 解析记录说明服务端至少做了 DNS 解析可能后续连接被网络策略拦截如果还收到了 HTTP 请求说明服务端真正发起了 TCP 连接甚至发送了完整 HTTP 请求如果连 DNS 解析都没有那就要怀疑参数本身没有被服务端当作 URL 处理或者参数名不对。带外通道不仅能用于判断漏洞存在也能用于数据外带。比如在内网读到的文件内容可以通过把内容作为域名/子域名的前缀拼接到带外域名上再通过 DNS 解析记录把内容带回来。比如目标内网有个文件叫/etc/passwd你把它读出来然后把第一个字符作为子域前缀请求一次DNS 日志里就能看到。虽然一次只能带一点数据但在无回显场景下这就是一条可靠的数据通路。3.2 时间差测量用响应时长当“眼睛”除了带外通道时间差是另一种非常实用的盲信号。很多协议在连接不同端口或发送不同数据时响应时间有明显差异。这种差异可以被当作一种“二进制信号”来使用是/否、通/不通、匹配/不匹配。比如我想探测内网某个 IP 的端口开放情况可以直接构造一个带有连接超时差异的请求。如果端口开放服务端会在短时间内返回某种协议响应比如 RST 或完整握手如果端口不通服务端需要等待 TCP 超时才会返回错误。这个时间差有时候差几百毫秒在多次测量下会比较明显。再比如通过 SSRF 访问一个内网 URL如果路径存在且响应内容大耗时相对长一些如果路径不存在返回 404耗时就会短。这种差异看起来很小但放在自动化里做多轮测量就能比较稳定地判断目标路径是否存在。时间差测试需要注意的是网络抖动可能带来很多噪声。我一般会连续测量 5 次以上取中位数而不是看单次结果。另外如果目标服务端本身有缓存层第一次请求和后续请求的耗时差异很大那就要特别注意控制变量尽量保证同一个请求被同等处理。3.3 盲打与少回显利用错误信息、内容差异与测信道无回显的另一种“变体”是少回显也就是服务器不会返回完整响应但会返回状态码、响应时间、报错信息、甚至Content-Length的差异。这个“少回显”经常被忽视但它其实是很好的探测信号。举个例子。目标有一个网页截图服务它在截图失败时返回的错误信息里可能包含目标服务器的响应码比如upstream connect error: Connection refused和upstream connect error: 404 Not Found这两者返回的错误描述是不同的。虽然你没有拿到目标页面的正文但状态码已经告诉了你目标服务是否开放、路径是否存在。把这些信息组合起来完全可以在无回显条件下完成一次比较完整的资产盘活。另外还有一种偏测信道的思路那就是“布尔条件 时间差”组合。打个比方先让服务端去做一次请求如果这个请求的响应里包含某个特定字符串那么服务端继续执行一个耗时操作比如 SLEEP 几秒如果不包含则立刻返回。这样看起来服务端完全没有回显内容但你通过响应时长的长短就能推断出目标响应的内容特征。这在利用某些 HTTP 客户端库的“自定义请求后处理逻辑”时能够实现。这类思路在实战中不一定每次都成立因为需要目标代码里存在“根据响应内容做不同处理”的逻辑。但作为排查链路的一部分我通常会在初步判断出目标是一个无回显 SSRF 时先验证它是否支持这种“条件差异”。往往在深入测试几个点之后会发现原本以为的“无回显”其实存在一个非常窄的信息通道。3.4 收集信息的替代手段从日志与流量反推还有一种经常被忽略的手段利用目标系统本身的日志、监控、审计能力来反推攻击效果。比如 SSRF 打到了内网的 Redis虽然没有回显但如果你后续能看一眼 Redis 的日志或者目标站点能通过某种方式暴露出被写入的文件访问情况那么就相当于间接回显了。真实场景中我遇到过一种情况SSRF 无回显但是目标站点会定期把某些内网服务的数据聚合到一个页面上。我通过 SSRF 修改了内网某个服务的数据然后去刷新聚合页面发现内容变了。这就等于用“目标自身的业务页面”作为回显通道。这个思路总结成一句话不要只盯着“当前请求的响应”要把“请求产生的后果”当成一种信号来源。在无回显场景下这个“后果”可能是目标页面的内容发生变化目标系统的日志里多了一条记录目标服务的状态发生变化比如端口关闭、进程退出目标文件系统里多了一个文件目标数据库里多了一条数据。这些后果只要有一个能被你观察到无回显就等于被打开了口子。实际测试时我甚至会故意去打一些“无害修改型”的请求然后观察目标是否出现可见变化以此建立通道。4. 实操演练一个最小靶场的完整复现前面讲了很多理论但 SSRF 这种东西光看原理是学不会的。我在这里搭一个最小的可重复实验环境把从探测到利用到盲打的全过程走一遍。你可以按照这个流程在自己本机复现作为学习的基础场景。4.1 搭建可控的环境与靶标模拟我不会直接说明某个商业产品的配置而是用一个自己写的最小服务来模拟脆弱点。这样你能清楚看到每一层是怎么处理的也方便后续改造成其他协议。为了安全我把所有实验限制在本地回环地址127.0.0.1和自定义内网地址段内不涉及任何外部真实系统。靶场结构大致是三台“逻辑机”一台模拟“边缘应用”对外提供一个 URL 抓取接口一台模拟“内网 Redis 服务”端口 6379没有密码绑定在自定义内网网段一台模拟“带外日志平台”接收 DNS/HTTP 请求用于无回显观察。边缘应用的简化逻辑可以这样描述接收?target参数把它交给 HTTP 客户端去请求然后返回响应内容的前 200 字节。这就是一个典型的“有回显” SSRF。后续我会再加一层“只返回 OK”的逻辑模拟无回显。创建环境时我用 vagrant 或者本地容器方式启动三台“逻辑机”并自定义虚拟网段。这里的关键是让边缘应用可以访问到 Redis 所在的内网网段同时又不能从外部直接访问。这样才符合 SSRF 的真实场景。4.2 从探测到利用的完整流程第一步确认参数是否可被利用。我的习惯是先打一个自己可控的 HTTP 服务。如果边缘应用返回了你 HTTP 服务响应里的特征内容那就说明这个参数确实能被服务端请求。如果返回的是空白或 OK也不代表没有要继续用带外通道确认。请求示例 [边缘应用]/?targethttp://myhost/oob_test_123 观察myhost 上是否收到 /oob_test_123 的请求第二步探测端口与协议。在确认参数可控后尝试访问内网地址。先探测 Redis 端口是否开放请求示例 [边缘应用]/?targethttp://127.0.0.1:6379 观察响应内容里是否包含 Redis 版本、错误信息等。如果这里拿到的是Connection refused那就说明端口不通如果返回的是 Redis 的-ERR unknown command之类的信息说明端口开放。第三步构造 Gopher 载荷并发送。以写入一个 webshell 为例我需要构造一段 RESP 命令序列。先选定一个可以写入的绝对路径。Redis 的写文件一般用CONFIG SET dir和CONFIG SET dbfilename然后通过SET命令触发写盘。那么 RESP 序列大致是*4 $6 CONFIG $3 SET $3 dir $13 /www/wwwroot/然后再构造一个写入内容的命令。整个过程需要在本地先用脚本生成 Gopher URL然后放进边缘应用的目标参数里。注意这一步一定要先本地模拟验证因为一旦写入路径不对或内容格式不对就会破坏 Redis 数据影响后续实验。第四步确认利用结果。有回显场景下直接尝试访问 webshell 即可。无回显场景下我会用一个“再探测”的手段如果写入成功Redis 的响应会发生变化或者目标 Web 服务器会出现新的文件这个变化可以通过再次请求边缘应用的某个功能来间接观察。4.3 几个反复踩过的坑与排查手册这里整理一些我实操过程中踩过、也帮别人排查过的典型坑以表格形式列出来方便对照。现象可能原因排查思路Gopher 发出去后目标无反应载荷中换行符被二次编码检查 URL 编码是否为%0d%0a而不是%250d%250a在本地模拟先测试载荷带外只收到 DNS 没收到 HTTP服务端做了协议限制或网络策略确认目标实现是否能真正发起 HTTP 请求尝试换端口、换协议打 Redis 后 Redis 拒绝连接写盘命令导致 Redis 进程崩溃查看 Redis 日志确认写入路径有写权限优先用更轻量的命令验证连通性无回显判断不出漏洞参数名不对或服务端做了 URL 解码后的校验先用多个参数名批量测试再配合带外通道确认服务端跟随跳转导致校验绕过失败开发者在校验后没有控制跳转利用这一点构造中间跳转地址指向内网目标在无回显场景下最需要耐心。我的做法是每做一个操作前先想清楚如果这个操作成功会产生什么样的可观察副作用如果没有这个操作就不值得做。宁可慢一点也不要到处乱试否则很容易在对目标系统造成不可逆影响的同时一点有效信号都拿不到。5. 防御视角为什么修复常常不到位很多人以为 SSRF 的修复很简单加个黑名单、限制 IP 段、校验 URL 就行。但实际操作中修复遗漏点非常多。我见过的失败案例大致有这几类。5.1 常见过滤方案的漏洞点第一类是“黑名单校验”这是最脆弱的。很多系统只拦了127.0.0.1、localhost但完全忽略了127.0.0.0/8整个网段、IPv6 地址、十进制/十六进制 IP 写法、短域名、DNS 重绑定等变体。例如2130706433就是127.0.0.1的十进制表示不少黑名单根本识别不出来。同样[::1]也是回环地址如果解析器支持 IPv6黑名单形同虚设。第二类是“域名白名单校验但没校验跳转”。这是非常常见的情况。入口判定域名合法但请求发出后跟随了 301/302跳转到一个完全不合法的高风险地址。如果实现里没有对每一次跳转做校验这个白名单就是个摆设。第三类是“只校验了 scheme没校验资源位置”。比如允许file://或dict://那问题就大了。前文提到的 Gopher 之所以能打出效果往往就是因为 scheme 校验不完整。5.2 分层防御的组合拳设计与其依赖单一修复点我更推荐分层防御每一层都承担一部分职责即使某一层被绕过后续层仍然能够兜底。第一层是入口校验对用户输入的 URL 做完整的 scheme、host、端口白名单。这一层目标是拦截明显不合规的请求。第二层是出口控制把服务端发起请求的能力限制在一个可控的网络区域比如通过防火墙或安全组只允许边缘应用访问特定的目标服务。这样即使 SSRF 存在攻击者也很难打通到敏感资产。第三层是应用层对响应的处理不在页面里直接透传服务端响应内容限制响应长度、过滤错误信息降低利用者拿到有效信息的可能性。另外我特别推荐在架构层面尽量减少“无脑请求外部 URL”的功能。比如网页截图服务、PDF 导出服务这类高危险功能如果一定要做最好在一个独立的环境中运行不带云凭证网络访问权限最小化并且设置严格的资源配额和请求超时。很多时候SSRF 之所以造成严重破坏并不是因为漏洞本身多高级而是因为出问题的服务本身权限太大、位置太核心。5.3 修复后如何验漏修复不是改完代码就完事还需要验证。我建议在修复后做一轮针对性的回归测试重点覆盖以下场景常见回环地址变体127.0.0.1、127.0.0.2、0.0.0.0、[::1]、十进制 IP、十六进制 IPDNS 重绑定绑定一个域名第一次解析返回白名单地址第二次解析返回内网地址30x 跳转白名单域名跳到内网地址协议变体gopher://、dict://、file://、ftp://非标准端口把内网服务映射到 80/443 端口半回显信息泄露确认错误信息里不包含目标响应状态码或头信息。每一步都要实际打一遍确认防护确实挡住了。不要只写一个单元测试就收工因为 SSRF 涉及的外部解析、DNS 重绑定、网络拓扑等因素很难完全靠单元测试覆盖。实战验证才是更可靠的检验方式。我在实际项目里的体会是SSRF 是一个越深入研究越会觉得它“无底”的方向它不像某些漏洞一样有固定的答案和模板而是要充分理解业务逻辑、网络环境、协议栈以及服务端的使用方式。很多人学了几天就觉得会了可一到真实环境就发现根本没有回显、协议也被限制这时功夫就体现在你对协议细节和盲信号通道的把握上。如果你把这篇文章里的方法吃透至少再去遇到“无回显”的场景时不会一下子就放弃了。另外再分享一个小建议测试 SSRF 时每打一个目标前先在本地做一个同构模拟环境把请求原样重放一遍观察目标程序的行为。这样你不仅知道漏洞怎么打还能理解为什么打不通、卡在哪一层。很多高级技巧其实都是在“本地重放 真实环境对比”的过程中悟出来的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑