Oracle 发 9.1 分警报:Java 还在信任那批私钥可被算出的证书,我本机实测了

Oracle 发布 CVE-2026-91403 安全警报,CVSS 9.1,本质是 Java 还在信任 eMudhra 八张私钥可被算出的中间 CA。本机实测 OpenJDK 默认六条受损证书链全部信任,并逐一验证了两种缓解措施的实际效果。

10 月 21 日,Oracle 发布了一份计划外的安全警报,编号 CVE-2026-91403,CVSS 3.1 评分 9.1,影响 Oracle Java SE、Oracle GraalVM for JDK 与 GraalVM Enterprise Edition。IT之家在同一天报道了这条消息,标题落在「高危漏洞,用户应尽快升级」。

这条新闻本身没有错,但它把一个证书信任问题写成了「Java 出了个高危漏洞」的样子。主机吧读完原始公告后发现,实际情况值得多说几句——因为这个「漏洞」的成因,主机吧在一周前刚写过一篇:印度 CA 机构 eMudhra 的中间证书私钥可被直接算出来。CVE-2026-91403 就是那起事件的第三块多米诺骨牌。

更关键的是:浏览器早就封了,操作系统也封了,唯独默认配置的 Java 还在信任。我在本机做了实测,结果和厂商的说明完全对得上。下面先说结论,再说怎么处理。

这不是 Java 的代码缺陷,是 Java 的证书信任材料没人来更新。把一个「信任决策错误」按「远程代码执行」的口径去理解,后面的处置动作很容易做错。

一、先把这个漏洞的性质说清楚

Oracle 公告的描述只有一句话:

This Security Alert addresses CVE-2026-91403 by providing Java updates which distrusts the set of impacted intermediate CA certificates related to eMudhra.

没有内存越界,没有反序列化,没有可执行代码被注入。它做的是把一批证书加进黑名单。CVSS 9.1 的向量是 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N——网络可达、低复杂度、无需权限、无需交互,机密性与完整性双高,可用性无影响。这个打分在逻辑上没错:如果攻击者能签发任意域名的证书,你发给「银行」的请求可以被完整读取和篡改。

但这里有个容易踩的坑。OpenJDK 的公告写得比 Oracle 更明确:CVE-2026-91403 concerns eight compromised intermediate CA certificates.——它明确说这是一个证书信任问题。第三方安全分析也点出了同一件事:这条通告「并未确立一条 Java 远程代码执行的利用链」。

所以正确的理解是:

不是 而是
JVM 有个洞,攻击者能塞代码进来 JVM 会相信一张伪造的证书,因为它信任的中间 CA 已经不安全
服务器会被直接打穿 你的 Java 程序连接的服务,可能不是你以为的那个服务
打个补丁就完事 补丁改的是一个证书文件,怎么改取决于 Java 是怎么装的

换句话说,受害位置在 Java 作为 TLS 客户端的那一侧,不在服务器端。JSSE 这个组件名(Java Secure Socket Extension)已经说明了位置——就是 Java 里做 TLS 的那一层。

二、为什么浏览器拦住了,Java 没拦住

要理解这个 CVE,得先看清一条平时不会注意的分界线。

浏览器、操作系统、CA 三条封禁通道与 JVM 默认配置的断层示意
同一把受损密钥,三条封禁通道与 JVM 之间的断层

eMudhra 的八个中间 CA 出问题之后,各方反应其实很快:

  • 10 月 17 日,报告者用每个私钥签名了「We have a problem here」并提交给浏览器根计划,Firefox 与 Chrome 当天封禁;
  • 10 月 18 日至 19 日,eMudhra 以 CA_COMPROMISE 为原因撤销了全部八个中间证书;
  • 10 月 19 日 Apple 封禁,10 月 20 日 Microsoft 封禁。

这些封禁通过 OneCRL、CRLite、CRLSets 等各自渠道下发,都是浏览器自己的机制。而 JVM 有两个地方和它们完全不相通:

第一,Java 自带一份独立的 cacerts。除非应用显式设置 javax.net.ssl.trustStore,JDK 只用 $JAVA_HOME/lib/security/cacerts(或同目录的 jssecacerts)里那份根证书列表。它不订阅浏览器的封禁列表,也不跟随操作系统的 CA 更新——除非这个 JDK 是发行版包管理器装的、且 cacerts 恰好是指向系统证书库的软链。

第二,Java 默认不检查证书撤销状态。com.sun.net.ssl.checkRevocation 默认是 false。也就是说,即使 eMudhra 在 10 月 18 日已经把这些证书标成 CA_COMPROMISE,一个默认配置的 JVM 在握手时根本不会去问「这张证书撤销了吗」。CA 的撤销对一个默认 JVM 是隐形的。

还有第三层:Java 标准 TLS 实现不强制证书透明度(CT)。按测试站的说法,这些中间证书在封禁前从未出现在 CT 日志里——这意味着强制 CT 的浏览器不可能被用于攻击,但对不做 CT 校验的客户端来说,这条线索同样不存在。

三、本机实测:6 条链,全部信任

与其看别人的截图,不如直接在自己的机器上跑。我用的是环境里现成的 OpenJDK 20(Zulu 20.28+85-CA),先用 keytool 确认两个关键根证书都在:

keytool -list -cacerts -storepass changeit | grep -iE "emsign|comodo"
# comodoaaaca [jdk],      Jan 1, 2004, trustedCertEntry      ← Sectigo AAA
# cert_94_emsign_ecc_root_ca___g394, Feb 27, 2023, trustedCertEntry  ← emSign ECC Root CA - G3

两个根都在。注意 comodoaaaca 这一行——它就是 Sectigo 的 AAA Certificate Services。这一点很关键,后面会讲。

然后写一个最小程序,用 java.net.http.HttpClient 依次连接测试站提供的六个主机,覆盖 DV / OV / EV 三种证书等级 × emSign 与 Sectigo 两条签发路径:

var c = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
var r = c.send(HttpRequest.newBuilder(URI.create(url)).build(),
               HttpResponse.BodyHandlers.ofString());
return "TRUSTED  HTTP " + r.statusCode();

六个主机按 {dv,ov,ev}-{g3b,g3c}[-aaa]-<测试站域名> 的规律拼出,-aaa- 后缀表示走 Sectigo 的交叉签名路径。返回 200 就是「信任了」,抛异常才是「拒绝了」——这里要特别留神,默认配置下能连上才是坏结果。

本机 OpenJDK 20 实测:默认配置下 6 条 eMudhra 受损证书链全部被信任,加入 blocked.certs 后全部拒绝
OpenJDK 20 实测结果:默认全部信任,加入黑名单后全部拒绝

结果很干净:六个主机,六个 HTTP 200。一条都没拒。

这里面最值得单独说的是 -aaa- 那三行。很多人会想「我用的不是 eMudhra 的根,跟我没关系」——但 Sectigo 当年给同样的公钥做了交叉签名,所以走 Sectigo 根也能链到同一批中间证书。密钥是同一把,换根躲不过。主机吧此前在 eMudhra 那篇文章里提过这个交叉签名结构,这里是它在 Java 侧的实际体现。

四、两种缓解措施,我都试了

Oracle 和 OpenJDK 给了两条不必升级的应急路径。两条我都跑了一遍,都能挡住。

路径一:开启撤销检查。加两个系统属性:

java -Dcom.sun.net.ssl.checkRevocation=true \
     -Dcom.sun.security.enableCRLDP=true \
     Check.java

加上之后,六个主机全部被拒。其中 Sectigo 那条链的报错信息很明确:

CertificateRevokedException: Certificate has been revoked,
reason: CA_COMPROMISE, revocation date: ...

但另外几条链报的是 CertPathValidatorException: Could not determine revocation status——拒绝的理由不是「确认撤销」,而是「没法确认是否撤销」。这个区别很重要,下面第五节会展开。

路径二:替换 blocked.certs。这是 OpenJDK 公告的首选建议。它就是一个文本文件,第一行写 Algorithm=SHA-256,之后每行一个证书的 SHA-256 指纹。测试站公布了 impacted-intermediates.json,里面正好是 16 条记录——8 个中间 CA,每个都有 emSign 签名和 Sectigo 交叉签名两个版本。

# 先生成 16 条指纹,再追加到 JDK 的 blocked.certs
python3 -c "import json; [print(x['sha256']) for x in json.load(open('impacted-intermediates.json'))]" \
  >> "$JDK_HOME/lib/security/blocked.certs"

为了不动原环境,我拷了一份 JDK 出来单独改。改完之后六个主机全部 REJECTED,报错是:

CertPathValidatorException: Untrusted certificate: CN=EM DV TLS CA - G3B, O=eMudhra BV, C=NL

这个报错比撤销检查那条清楚得多——它直接点名了是哪张中间证书被拒。

然后是最重要的一步:验证没有误杀。我在两种配置下都跑了 https://www.zhujib.com/ 作为对照,都是 200。追加 16 条指纹只拉黑那八个中间 CA 本身,不影响其他证书——这也正是它比「直接删掉两个根」更精确的原因。

五、动手前,先把这三个坑填上

实测过程中遇到几个问题,都是照着文档做仍然会卡住的地方。

坑一:官方 blocked.certs 下载链接打不开

按 OpenJDK 公告给的地址取文件,Unix 和 Windows 两个版本我都试了,都返回 HTTP 403,页面是「Unexpected outage」。这不是我这边的问题——同一时段有研究者记录了完全一致的遭遇。所以现阶段更可靠的做法是从 impacted-intermediates.json 自己生成指纹追加,等官方链接恢复后再换回分发版本。

另外,Rev 1 的公告里还写错了一处:11.0.33.0.2 这个版本号后来在邮件列表里更正为 11.0.33。照着错版本号去找补丁会找不到。

坑二:撤销检查会让正常握手也失败

前面提到的那几条 Could not determine revocation status,就是这个问题。撤销检查会在每次连接时去拉 CRL,分发点不可达、超时或者返回异常,握手就直接失败——不是因为证书有问题,而是因为查不到状态。

实测里最直接的证据是:开启撤销检查之后,连对照组 www.zhujib.com 都报 ConnectException: HTTP connect timed out。同一台机器、同一个网络,不开撤销检查时它是 200。

这不是说撤销检查不该用,而是说它改变的是所有证书的校验行为,副作用范围远大于「只拉黑这八张证书」。生产环境里,如果你的服务依赖多个外部 HTTPS 接口,务必先在预发环境跑一轮,别直接上生产。

坑三:改错文件,改完等于没改

按安装来源分流:独立 JDK、发行版包、Homebrew 与 FreeBSD 三类环境的处置差异
按安装来源分流,三类环境改的不是同一个东西

OpenJDK 公告把处置方式按安装来源分成三类,这个分类比「升级 Java」这句笼统建议有用得多:

Java 怎么装的 用哪份证书 该做什么
独立安装(Oracle JDK、Temurin、Liberica、官网压缩包) 自带的 cacerts 升级到 .0.2 版本;做不到就换 blocked.certs 或开撤销检查
发行版包(Arch、Debian、Ubuntu、Fedora、RHEL、Oracle Linux 等) 多数是系统 CA 库 更新系统 ca-certificates 即可,Java 本身不用动
Homebrew、FreeBSD 仍是 Java 自带 必须更新 Java 包本身,更新系统证书无效

Homebrew 和 FreeBSD 这两行是明确的例外。如果你在 macOS 上用 Homebrew 装的 JDK,看到「更新 ca-certificates」这类建议照做,等于什么都没做。想快速判断自己的环境属于哪一类,看一眼 cacerts 是不是软链就有答案:

ls -l "$(dirname $(dirname $(readlink -f $(which java))))/lib/security/cacerts"

软链指向系统证书库的,走系统更新那条路;是普通文件的,就得动 JDK 自己。

还有一处小矛盾要提醒:OpenJDK 公告说受影响的版本是 8u512 及更早,而测试站把 8u511 列为已修复。这两个说法在写这篇文章时还没能对齐。用 JDK 8 的站点建议直接问发行方要确切版本号,别按公告里的数字去猜。

六、这件事真正的教训

eMudhra 事件(经过见主机吧此前的梳理)从 10 月 17 日爆发到现在,已经牵出了三条独立的信任链:浏览器、操作系统、Java 运行时。三方各自修复、各自发公告,节奏完全不同步。

主机吧觉得最有价值的一点是:「我升级了浏览器」和「我的 Java 服务安全了」是两件事,中间没有任何自动传递关系。Java 那份 cacerts 是个很安静的组件——它只在 JDK 更新或管理员手工改的时候变,其他时候一动不动,包括世界另一边有人把 CA 的私钥算出来的时候。

如果你手上有跑着 Java 的服务,尤其是那些会主动往外部发 HTTPS 请求的(调第三方 API、拉配置、连消息队列、下载依赖),今天就值得花十分钟做一件事:确认它的 JVM 到底用哪份信任库,以及那份信任库里那两张根证书还在不在。查询命令一行就够:

keytool -list -cacerts -storepass changeit | grep -iE "emsign|comodoaaaca"

如果有输出,说明你的 Java 此刻还在信任那批证书。有 blocked.certs 这条精准路径在,处理起来不算麻烦——前提是你知道自己要动的是哪个文件。

主机吧 · 本文实测基于 OpenJDK 20(Zulu 20.28+85-CA),数据引自 Oracle 安全警报、OpenJDK 2026/10/21 公告与 emudhra.we-have-a-problem.com 公布的受影响证书清单。

给TA打赏
共{{data.count}}人
人已打赏
在线客服
在线客服
热线电话
QQ客服
电子邮箱
suduwangluo