Java JDBC注入深度解析
概念
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 | <dependency> |
Demo:
1 | package JDBC; |
其中jdbc:mysql://表示要连接的数据库类型mysql,localhost:3306为mysql服务地址,dvwa为数据库名。简单查询中2表示查询的列索引,即第2列
SPI机制
Service Provider Interface,是JDK内置的一种服务提供发现机制,可以用来启动框架扩展和替换组建。服务提供接口,不同厂商可以针对同一个接口做出不同的实现。当服务提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录下创建一个以服务接口命名的文件,文件内容就是这个接口的具体实现类,当程序需要这个服务时,就可以通过查找这个jar包的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化。
源码分析
环境:mysql-connector-java 5.1.28
加载驱动
1 | String CLASS_NAME = "com.mysql.jdbc.Driver"; |
也可以写成:
1 | DriverManager.registerDirver(new Driver()); |
下面我们跟下代码看下究竟怎么实现的
当JVM加载com.mysql.jdbc.Driver类到内存中时会自动触发其静态初始代码块:
调用java.sql.DriverManager.registerDriver(new Driver());方法来注册驱动,要注册的驱动即是com.mysql.jdbc.Driver对象:
调用另一个重载方法registerDriver:
方法中实例化了一个DriverInfo对象用来存放驱动类信息。
registeredDrivers是DriverManager对象的一个变量,通过addIfAbsent方法,将DriverInfo对象信息存放进registeredDrivers变量中
获取连接
1 | String URL = "jdbc:mysql://localhost:3306/dvwa"; |
跟进getConnection方法:
其中Reflection.getCallerClass()用于获取调用者的类,即在运行时确定正在调用该方法的类的名称,返回一个Class对象,比如我这里是JDBC.test
跟进getConnection重载方法:
先获取类加载器
遍历registeredDrivers变量的值,里面存放着我们注册过的驱动,随即通过DriverInfo对象获取对应驱动
然后就是通过获取的驱动,调用其connet方法进行连接,由于驱动com.mysql.jdbc.Driver没有该方法,所以调用父类NonRegisteringDriver.connect方法:
先对 URL 前缀进行判断,判断成功直接路由到专用方法
再通过parseURL()将 URL 和 info 合并解析为统一的 Properties 对象:
NUM_HOSTS_PROPERTY_KEY表示 URL 中配置的主机数量,如果主机数大于1就调用connectFailover处理故障转移场景
最后调用com.mysql.jdbc.ConnectionImpl.getInstance方法获取连接:
这部分逻辑就是判断JDBC版本是否在4以上,JDBC4以上调用Util.handleNewInstance通过反射动态创建增强版连接对象,JDBC_4_CONNECTION_CTOR静态常量指向com.mysql.jdbc.JDBC4Connection 的构造函数,最后返回Connection对象:
handleNewInstance用于反射实例化
相当于调用JDBC4Connection.newInstance(args),传入参数为创建连接的信息如主机名端口等等:
会调用父类ConnetionImpl的构造方法:
获取主机名,端口,数据库名等信息
这段代码用于创建连接,this.dbmd = getMetaData(false, false);初始化数据库元数据对象。initializeSafeStatementInterceptors();初始化”安全模式”下的SQL拦截器,createNewIO(false);用于建立物理数据库连接,unSafeStatementInterceptors();启用常规SQL拦截器
跟进createNewIO(false);:
先获取连接信息,然后调用connectOneTryOnly(isForReconnect, mergedProps);方法:
coreConnect(mergedProps);执行底层 TCP 握手 + MySQL 协议认证,即建立连接
将用户配置的 Statement 拦截器绑定到底层 IO 层
就是在建立TCP连接后对数据库会话进行“上下文初始化 和 状态恢复”并设置拦截器、读取服务器配置等作用
回到Connectionlmpl方法:
最后将新创建的连接对象注册到驱动的跟踪系统中并进行资源管理
扩展参数带来的安全问题
mysql JDBC中包含一个危险的扩展参数:autoDeserialize。这个参数配置为true时,JDBC客户端会自动反序列化返回的BLOB类型字段,BLOB为二进制形式的长文本数据
JDBC反序列化
由于后续mysql-connector-java会用到多个不同版本,所以依赖设置为:
1 | <properties> |
当需要切换版本时终端输入:
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);方法:
跟进后发现调用了connectOneTryOnly(isForReconnect, mergedProps);方法:
connectOneTryOnly方法中coreConnect(mergedProps);用于建立连接,会调用initializePropsFromServer方法,initializePropsFromServer方法内又会调用buildCollationMapping方法:
在buildCollationMapping方法中,stmt通过getMetadataSafeStatement()方法获取当前环境的StatementImpl对象,然后通过executeQuery方法执行SQL语句
然后执行Util.resultSetToMap方法,versionMeetsMinimum方法用于判断驱动版本:
在Util.resultSetToMap方法中会将SHOW COLLATION查询结果的第三列和第二列的值存放进mappedValues
还会调用ResultSetImpl对象的getObject方法,对应反序列化位置,需要字段类型为blob:
当字段为BLOB类型且扩展参数autoDeserialize为true,且SHOW COLLATION的返回结果需要有三个字段,就会从MySQL服务端中获取对应的字节码数据,且需要字段2或3为BLOB装载我们的序列化数据,那么在返回数据时就会触发反序列化造成攻击
这里使用4ra1n师傅做的mysql-fake-server项目来进行复现:
exp:
1 | package JDBC; |
mysql-connector-java 5.1.29~5.1.40
环境:mysql-connector-java 5.1.29
改进部分在于com.mysql.jdbc.ConnectionImpl#buildCollationMapping方法:
不仅需要驱动版本大于4.1.0,还添加了新的要求:
getDetectCustomCollations方法返回扩展参数detectCustomCollations的值,若没设置,默认为false
只需要新添加扩展参数detectCustomCollations=true即可,这样才能进入Util.resultSetToMap方法调用getObject触发反序列化
exp:
1 | package JDBC; |
mysql-connector-java 5.1.41~5.1.48
环境:mysql-connector-java 5.1.41
依旧看改进的部分,定位到com.mysql.jdbc.ConnectionImpl#buildCollationMapping方法:
新加了customCharset变量需要为null,其默认为null,所以不用管
这里没有调用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方法:
indexToCharset默认为null不用管,同时需要detectCustomCollations为ture
调用了ResultSetUtil.resultSetToMap方法:
同样调用了getObject方法
这里需要字段类型为BIT类型,后面的调用就类似的,所以利用链不影响
exp:
1 | package JDBC; |
ServerStatusDiffInterceptor链分析
mysql-connector-java 5.1.0~5.1.10
环境:mysql-connector-java 5.1.1
在较低的mysql-connector-java版本下是不能利用datectCustomCollations链的:
所以这时就可以利用ServerStatusDiffInterceptor链
利用条件:需要连接后进行查询
1 | String sql = "select database()"; |
或者
1 | Statement statement = connection.createStatement(); |
是在查询时触发反序列化,而前者是获取结果处触发
执行完查询后,重点在获取结果位置,跟进executeQuery方法:
locallyScopedConn.useMaxRows方法默认返回false,会调用executeInternal方法:
利用当前连接对象调用execSQL方法,即ConnectImpl#execSQL:
接着执行sqlQueryDirect方法:
statementInterceptors不为null时调用invokeStatementInterceptorsPre方法:
这里对statementInterceptors进行遍历,但默认状态下statementInterceptors为0,所以需要找个方法控制它的值
MysqlIO类提供initializeStatementInterceptors方法来初始化StatementInterceptors:
而在ConnectionImpl类构造方法中,在执行createNewIO方法时实例化了MysqlIO对象:
最后又刚好触发了MysqlIO对象这个方法:
而ConnectionImpl类下的StatementInterceptors可以通过添加扩展参数设置
这个参数首先必须实现com.mysql.jdbc.StatementInterceptor接口,这里选用com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor这个类,那么在invokeStatementInterceptorsPre方法中,会调用ServerStatusDiffInterceptor#preProcess方法:
驱动版本必须大于5.0.2,然后调用populateMapWithSessionStatusValues方法:
执行Util.resultSetToMap方法:
只需要SHOW SESSION STATUS语句返回的字段1或2的类型为blob,且内容为恶意的序列化数据即可
exp:
1 | package JDBC; |
mysql-connector-java 5.1.11~5.x.xx
环境:mysql-connector-java 5.1.29
定位到ConnectImpl#initializePropsFromServer方法:
跟进loadServerVariables方法:
执行executeQuery方法,跟进:
调用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方法
但其中调用了handleAutoCommitDefaults方法可以利用:
resetAutoCommitDefault会被赋值为true然后调用setAutoCommit方法:
needsSetOnServer赋值为true,会调用execSQL方法,跟进:
跟上面不同,调用的对象不同了,这里要利用的是sendQueryString方法:
调用sendQueryPacket方法:
跟上面的invokeStatementInterceptorsPre很像,跟进invokeQueryInterceptorsPre方法看看
但前提是queryInterceptors不为null
这里可以通过ConnectionImpl#connectOneTryOnly方法控制,当调用ConnectionImpl构造方法时会调用createNewIO从而调用connectOneTryOnly方法:
同样可以通过扩展参数设置
进入invokeQueryInterceptorsPre方法:
逻辑跟之前版本差不多,调用了preProcess,设置queryInterceptors为com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor,跟进ServerStatusDiffInterceptor#preProcess方法:
跟进populateMapWithSessionStatusValues方法:
执行ResultSetUtil.resultSetToMap方法:
最后执行了getObject方法
exp:
1 | package JDBC; |
mysql-connector-java 8.0.20以后
populateMapWithSessionStatusValues方法下不再调用ResultSetUtil.resultSetToMap方法,也不直接调用getObject方法:
bypass
环境:mysql-connector-java 8.0.12
Urlencode绕过
协议头Urlencode
逻辑位于com.mysql.cj.conf.ConnetionUrlParser#isConnetionStringSupported方法:
其中会对匹配部分进行一次decode方法处理,也就是一次Urldecode处理:
path部分Urlencode
辑位于com.mysql.cj.conf.ConnectionUrlParser#parseConnectionString方法:
path部分也进行了一次decode方法处理,此外还进行了trim方法处理,但方法不能去除字符串中间的空白字符,所以只能进行Urlencode绕过
扩展参数Urlencode
在实例化SingleConnectionUrl对象时,会触发父类构造方法:
调用collectProperties方法:
跟进getProperties方法,一直调用到parseQuerySection方法:
方法判断URL是否存在扩展参数,存在则调用processKeyValuePattern方法:
分离key和value,然后进行一次Urldecode,同样可以进行Urlencode编码
扩展参数Value绕过
由于com.mysql.cj.conf.BooleanPropertyDefinition的AllowableValue枚举类:
所以设置TRUE和设置YES是一样的
解析时还会转大写























































































