前言
最近在做ctf题时发现关于mysql任意文件读取漏洞的考点非常频繁,而且一直都朦胧不清,也没去学习,在不久前的DDCTF和国赛,还有最近的Nu1lCTF中都考到了这个点,利用Load data infile语法。在mysql客户端登陆mysql服务端后,客户端执行语句Load data local infile '/etc/passwd' into table proc;
,从而可以导致mysql进行本地或远程读取文件。这个原漏洞被爆出在去年phpmyadmin任意文件读取漏洞。
phpMyAdmin开启远程登陆导致本地文件读取
下面我们先去分析复现下这个漏洞。
如果phpmyadmin开启了如下选项
$cfg['AllowArbitraryServer'] = true; //false改为true
则登录时就可以访问远程的服务器。当登陆一个恶意构造的Mysql服务器时,即可利用load data infile读取该服务器上的任意文件。当然前提条件是secure_file_priv
参数允许的目录下,且phpmyadmin的用户对该文件有读的权限。
这里利用vulnspy上的实验环境演示分析该漏洞。
VulnSpy 已为大家提供在线 phpMyAdmin 环境地址:https://github.com/vulnspy/phpmyadmin-4.8.4-allowarbitraryserver
漏洞细节
LOAD DATA LOCAL导致的任意文件读取是个由来已久的问题,根据前人们的研究:
Read MySQL Client’s FileMySQL connect file read 我们知道下列的下列情况都存在该问题:
MySQL Client
PHP + mysql/mysqli
PHP + PDO (MYSQL_ATTR_LOCAL_INFILE)
Python + MySQLdb
Python3 + mysqlclient
Java + JDBC Driver
phpMyAdmin 属于典型的 php+mysqli 组合,当 AllowArbitraryServer 开启的情况下(默认关闭),我们可以让phpMyAdmin连接到恶意的MySQL服务器来触发任意文件读取漏洞。
漏洞利用
EXP: https://github.com/Gifts/Rogue-MySql-Server/blob/master/rogue_mysql_server.py
1. 首先是配置恶意服务器。在db服务器的命令行里修改root/exp/rogue_mysql_server.py文件,设port为3306外的其他端口,这里设为3307,然后在filelist中选择一个要读取的文件。我们这里读取/etc/passwd文件。
2. 运行python rogue_mysql_server.py,启动服务,服务会监听3307端口。
3. 打开phpMyAdmin的登录页面,地址输入db:3307、用户密码随意输,提交登录。
然后会发现生成一个mysql.log日志,查看日志
在日志中我们看到成功读取了passwd文件。
参考https://www.vulnspy.com/cn-phpmyadmin-load-data-local-file-read-local-file/
漏洞防御
关闭local_infile参数,禁止导入本地文件
开启–ssl-mode=VERIFY_IDENTITY参数,防止连接不安全的mysql服务器
参考https://www.anquanke.com/post/id/173039
ctf例题分析
下面看一下今年ddctf中mysql弱口令这道题,这题就是利用了以上原理。
http://117.51.147.155:5000/index.html#/scan 首先题目要求我们输入自己的服务器ip以及端口,并且要在服务器上运行mysql服务。
agent.py文件
#!/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("self.headers :", self.headers)
print("<----- Request End -----\n")
self.send_response(200)
self.send_header("Set-Cookie", "foo=bar")
self.end_headers()
result = self._func()
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("<----- Request End -----\n")
self.send_response(200)
self.send_header("Set-Cookie", "foo=bar")
self.end_headers()
result = self._func()
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()
这个agent.py代码不是很长,从模块名、函数名大概就能猜到它的意思——开启运行主机上的8123端口做一个http服务器,然后返回运行主机上的tcp进程信息。即代码_func方法中popen函数执行的命令netstat -tlnp,关于linux netstat命令参数的详解可见Linux netstat 命令
这里运行一下netstat -tlnp
agent.py代码的意思理解清楚后
按照提示把agent.py部署到我的阿里云主机上,记得安全组把8123端口打开,然后在自己的vps上运行agent.py以及伪造的mysql服务端https://github.com/Gifts/Rogue-MySql-Server。
然后在题目输入框输入你的vps的地址和你伪造的mysql服务端服务的端口后进行扫描。然后题目提示服务器未开启mysql。
这里就应该可以猜到,题目扫描的流程——先向目标ip的8123端口进行访问,获取目标vps上开启的tcp进程,然后进行判断mysql服务是否开启。
所以这里首先要做的是绕过这个判断,我们可以修改agent.py中的代码,从vps上的agent.py的输出结果来看,题目服务器应该使用GET型进行请求。
于是我们找到agent.py代码里的GET处理函数,修改如下图,将返回结果直接赋为result = [{'local_address':"0.0.0.0:3306","Process_name":"1234/mysqld"}]
def _func(self):
netstat = Popen(['netstat', '-tlnp'], stdout=PIPE)
netstat.wait()
ps_list = netstat.stdout.readlines()
result = [{'local_address':"0.0.0.0:3306","Process_name":"1234/mysqld"}]
# 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
self.wfile.write(json.dumps(result))
改为之后,我们再在伪造mysql服务器的脚本里改要读的文件名称,这里直接给含flag目标文件吧,~/.mysql_history
(root用户的mysql操作一般记录在该文件中)
1.开启agent.py,监听8123端口
2.开启rogue_mysql_server.py(监听端口3306、filelist为~/.mysql_history)
3.题目输入框输入vps ip及上述rogue_mysql_server.py(注意在自己的云主机上安全组中设置允许通过该端口),进行扫描
可以看到扫描成功,此时在rouge_mysql_server.py生成的mysql.log中应该就能看到~/.mysql_history文件内容,在里面可以找到flag。