作者:来自 Elastic Ruben Groenewoud, Terrance DeJesus, Bryan Porras Blanch, Eric Forte
这不是另一个 Log4Shell。Log4Shell(CVE-2021-44228) 是由日志记录字符串触发的 JNDI 查找。而 8 月报告的这个绕过问题是 Log4j 2 的 FilteredObjectInputStream 中存在的 Java 反序列化漏洞。我们在官方 Maven Central 提供的 log4j-api 和 log4j-core 2.26.1 上复现了这一问题。要进一步实现命令执行,还需要另外两个 Log4j 本身没有提供的条件。你需要一个仍然会对 Java 序列化的 LogEvent 对象进行反序列化的进程,还需要一个已经存在于该 JVM 中的 gadget 库。Apache 不会将这类 Log4j 允许列表绕过归类为产品漏洞,因为 Log4j 在正常日志记录过程中不会对数据进行反序列化。在本文中,我们将介绍:
-
该机制,引用官方
2.26.1源代码,以及它与 Log4Shell 的区别。 -
我们在实验环境中观察到和没有观察到的情况,包括实现命令执行所需的两个条件。
-
哪些 Elastic 规则会在后续利用阶段触发。
-
用于查找 Java 启动的 shell 以及遗留的序列化
LogEvent收集器的查询。
为什么 Apache 不将其归类为 Log4j 漏洞
Apache 发布的 CWE-502 说明非常明确:Log4j 在正常日志记录过程中不会对数据进行反序列化;FilteredObjectInputStream 用于帮助那些仍然会对日志事件流进行反序列化的应用程序;对该允许列表的绕过被视为安全加固问题,而不是产品漏洞;负责反序列化的应用程序有责任验证字节流。
本文基于公开资料、Apache 自己的安全页面、讨论 logging-log4j2#4168,以及我们在官方 2.26.1 JAR 包上进行的实验。我们并不声称能够完整检测所有自定义反序列化器。
这个 Java 反序列化漏洞与 Log4Shell 有什么不同?
Log4j 2 可以通过网络发送日志事件,例如使用 SocketAppender。另一个独立进程可以接收这些事件。过去一种接收方式是使用 Java 序列化:读取 ObjectInputStream,然后转换为 LogEvent。
这种接收方式正是 CVE-2017-5645 的工作方式。Apache 的安全公告指出:当 TCP 或 UDP socket server 接收到序列化的日志事件时,攻击者可以通过构造恶意二进制负载来执行代码。受影响的 log4j-core 版本为 [2.0-alpha1, 2.8.2)。修复程序随 2.8.2 发布,而该修复正是引入 FilteredObjectInputStream 的版本,该类最初位于 log4j-core(org.apache.logging.log4j.core.util.FilteredObjectInputStream)中。Java 7 及更高版本的用户被要求升级,或者停止使用这些 socket server 类。
FilteredObjectInputStream 是围绕这种"反序列化日志事件"机制提供的允许列表包装器。当前版本的类位于 log4j-api 中,其 2.26.1 源代码标记为 @since 2.11.0;更早版本的 FilteredObjectInputStream 则在 2.8.2 中作为 CVE-2017-5645 修复的一部分加入 log4j-core。它重写了 resolveClass(),拒绝不在允许列表中的类名,同时也允许调用方额外指定类。
Issue #4255 讨论的是这个允许列表,而不是 JNDI 或 LDAP。Elastic 2021 年的文章《使用 Elastic Security 检测 CVE-2021-44228(Log4j2)的利用》 则介绍了 Log4Shell。
一位 Log4j 贡献者已经在 讨论 #4168 的 发现 1 中记录了相同的允许列表逃逸方式。该讨论发表于 2026 年 7 月 1 日(MarshalledObject.get() 会在没有 Log4j 过滤器的情况下进行反序列化)。该讨论将这项工作定位为 2.x 的安全加固。之后, Issue #4255 对相同路径进行了命名。
Java 反序列化允许列表绕过是如何工作的
这里的每一个组件单独来看都不是漏洞。这个绕过问题来自官方 2.26.1 源代码中的三个行为。将它们串联起来后,攻击者控制的对象图就可以在没有应用任何过滤器的情况下完成反序列化。具体如下:
| 步骤 | 组件 | 行为 | 为什么重要 |
|---|---|---|---|
| 1 | SerializationUtil.REQUIRED_JAVA_CLASSES |
java.rmi.MarshalledObject 作为 Message 委托对象位于允许列表中 |
该承载类按设计就是允许的 |
| 2 | FilteredObjectInputStream.resolveClass() |
只检查它所读取的流中存在的类描述符 | 作为不透明 byte[] 保存的负载永远不会被检查 |
| 3 | Log4jLogEvent.LogEventProxy.message() |
外层流接受代理对象后,调用 marshalledMessage.get() |
在一个没有附加过滤器的新流上对这些字节进行反序列化 |
1. 允许列表包含 java.rmi.MarshalledObject。
SerializationUtil.REQUIRED_JAVA_CLASSES 将它与注释 for Message delegate 一起列出,同时还包括 BigDecimal、BigInteger 和基本类型名称。允许的包包括 java.lang.、java.time.、java.util. 和 org.apache.logging.log4j.。
我们于 2026 年 8 月 26 日从 apache/logging-log4j2 的 2.x 分支获取了相同的列表。
2. FilteredObjectInputStream.resolveClass() 只能看到该流中的类描述符。
java
`
1. protected Class<?> resolveClass(final ObjectStreamClass desc)
2. throws IOException, ClassNotFoundException {
3. final String name = SerializationUtil.stripArray(desc.getName());
4. if (!(isAllowedByDefault(name) || allowedExtraClasses.contains(name))) {
5. throw new InvalidObjectException("Class is not allowed for deserialization: " + name);
6. }
7. return super.resolveClass(desc);
8. }
`Lobster AI
来源:FilteredObjectInputStream.java,位于 rel/2.26.1。MarshalledObject 将其负载作为不透明的 byte[] 携带。这些内部类名称不会经过这个 resolveClass()。
3. LogEventProxy 随后调用 MarshalledObject.get()。
Log4jLogEvent.LogEventProxy 从 2.8 开始就一直持有 private MarshalledObject<Message> marshalledMessage。外层流接受代理对象后,readResolve() 会构建一个 Log4jLogEvent,而 message() 会执行以下操作:
csharp
`
1. private Message message() {
2. if (marshalledMessage != null) {
3. try {
4. return marshalledMessage.get();
5. } catch (final Exception ex) {
6. // ignore me
7. }
8. }
9. return new SimpleMessage(messageString);
10. }
`Lobster AI
来源:Log4jLogEvent.java,位于 rel/2.26.1。get() 抛出的任何异常都会被处理。接收端仍然可能看起来运行正常,并回退到 SimpleMessage(messageString)。
MarshalledObject.get() 会在内部的 ObjectInputStream 上对 objBytes 进行反序列化,而这个流不是 FilteredObjectInputStream。在 JDK 9 及更高版本中,MarshalledObject 可以复制外层流的 JEP 290 ObjectInputFilter。2.26.1 中的 FilteredObjectInputStream 没有调用 setObjectInputFilter(),因此除非其他地方安装了过滤器,否则该复制的过滤器为 null。讨论 #4168 也说明了 Java 8 与 Java 9+ 之间的这一差异。
Log4j 已经存在一条嵌套对象路径,可以重新应用 过滤器:SerializationUtil.writeWrappedObject / readWrappedObject。而 marshalledMessage.get() 没有使用这条路径。这正是讨论 #4168(发现 1)中建议的方向之一,同时还建议从允许列表中移除 MarshalledObject。
当前 2.x 的 log4j-core 源代码中已经删除了 TcpSocketServer / ObjectInputStreamLogEventBridge。对于当前版本,剩余的暴露面是那些遗留代码或自定义代码,它们仍然执行 new FilteredObjectInputStream(socket.getInputStream()),然后通过 readObject() 将数据读取为 LogEvent。
历史上的 TcpSocketServer 会构造新的 ServerSocket(port),监听该主机的所有网络接口。例外是 2.8.2:在该版本中,TcpSocketServer 和(log4j-core 中的)FilteredObjectInputStream 都随 core 一起发布,因此使用内置 socket server 的标准部署即处于暴露状态,不需要任何遗留或自定义接收器。#4255 绕过了 CVE-2017-5645 的修复,而且发生在引入该修复的同一个版本中。
某个遗留收集器是否可以在无需凭证的情况下被访问,取决于它的部署方式。我们并不是说所有收集器都没有身份验证。
我们在 Log4j 2.26.1 上复现了什么
我们使用了官方 Maven Central 中的 log4j-api 2.26.1 和 log4j-core 2.26.1。接收端实现了公开的 FOIS 加 LogEvent 读取逻辑,并在测试期间绑定到回环地址。
-
一个
FilteredObjectInputStream会在外层流中拒绝的类,在被放入序列化的Log4jLogEvent中的MarshalledObject携带后,成功执行了一次。 -
在该实验环境中,要实现命令执行,需要同时具备 Java 序列化的
LogEvent接收端,以及已经存在于受害者 JVM 中的 gadget 库。仅有log4j-api和log4j-core并不足以实现命令执行。 -
在同一个 FOIS 实例上安装 JEP 290
ObjectInputFilter后,我们使用的负载被拒绝。在调用该方法之前,流过滤器为null。
展示 Log4j 反序列化命令执行所需条件的表格:序列化的 LogEvent 接收端和 gadget 库
受影响的 Log4j 2 版本
我们检查了多个 JAR 版本,以确认 log4j-api 中是否同时存在该字段和 FOIS。
| 检查的版本 | marshalledMessage 字段 |
log4j-api 中的 FilteredObjectInputStream |
|---|---|---|
2.6.2、2.7 |
不存在 | 不存在 |
2.8 |
存在 | 不存在 |
2.8.2 |
存在 | 存在(log4j-core) |
2.9.1、2.10.0 |
存在 | 不存在 |
2.11.0 至 2.26.1 |
存在 | 存在(log4j-api) |
该绕过需要同时存在 marshalledMessage 字段和 FilteredObjectInputStream,后者必须是被绕过的组件。这两个条件在 2.8.2(FOIS 位于 log4j-core)以及 2.11.0 至 2.26.1(FOIS 位于 log4j-api)中同时满足;我们确认了 2.8.2 以及 2.11.0--2.26.1 上的命令执行。2.9.1 和 2.10.0 包含该字段,但没有 FOIS,因此不适用这一特定的允许列表绕过。我们没有测试每一个 2.x 版本,也没有测试 Log4j 3(讨论 #4168 表示其中已经移除了序列化)。
防御方面需要关注的是类路径:如果负责反序列化 LogEvent 的 JVM 同时包含一个已知的 gadget 库,那么就存在命令执行的可能。如果没有,那么内部的 get() 仍然可能在该 JVM 实际存在的其他组件中执行未经筛选的代码。
设置 Java 反序列化攻击检测
-
在运行 JVM 的主机上部署 Elastic Defend,并启用进程和网络事件(如果可以,使用 Complete EDR)。
-
启用 Detection 中列出的预构建 SIEM 规则。
-
梳理会反序列化 Java 序列化
LogEvent的进程:遗留的TcpSocketServer、log4j-server示例、socket 上的SerializedLayout,或者任何自定义的FilteredObjectInputStream+LogEvent桥接程序。JSON、syslog 和 HTTP 日志采集属于不同路径。
针对 Java 反序列化攻击后利用活动的 Elastic 检测规则
Potential Reverse Shell via Java 会查找 Java 网络事件(connection_accepted 或 connection_attempted),并在之后 5 秒内查找父进程为带有 -jar 参数的 java 的 shell。该规则会排除私有地址和回环地址的 destination.ip 范围,包括 10.0.0.0/8、192.168.0.0/16、172.16.0.0/12 和 127.0.0.0/8。Potential Reverse Shell Activity via Terminal 是另一个相关规则,在我们的测试过程中也被触发。
因此,当 java -jar 进程与一个_公网_目标建立通信,随后启动 shell 时,该规则可能触发。以下情况则不会触发:
-
仅使用回环地址的采集器(我们的实验环境就是这种绑定方式)。
-
使用内部 RFC1918 地址的采集器,而这恰恰是你在局域网中预期可能存在的遗留 socket 服务器场景。
-
没有使用
-jar启动 JVM 的情况(模块路径、org.apache.logging.log4j主类以及许多应用服务器的启动方式)。
Suspicious Child Execution via Web Server 只有在父进程参数匹配已知服务器启动器时,才会包含 process.parent.name == "java",例如 Tomcat Bootstrap、Jetty、WildFly、Spring Boot loader、Jenkins .war 等。独立运行的 FOIS 采集器通常不在这个列表中。
我们建议在面向 Internet 的主机上运行以下规则:
如果你看到 java 派生 bash/sh,但没有 LDAP/RMI/DNS 前置活动,不要直接将其认定为 Log4Shell 漏洞利用遗漏。应将其视为 Java 反序列化或注入路径触发 gadget 执行的潜在迹象,并检查该 JVM 是否充当日志事件接收器。
搜索 Java 反序列化攻击
搜索查询可能产生高价值信号,也可能存在误报。这些查询用于识别潜在的可疑行为,但仍需要进一步调查才能确认结果。
搜索遗留的序列化 LogEvent 采集器
目标:查找能够_接受_ TCP 连接的 JVM,尤其是在本不应该充当日志事件中心的主机上。
Persistence Through Reverse/Bind Shells 提供了针对 listening_ports、process_open_sockets 和 processes 的 Osquery。可以先用它进行资产清点,然后筛选出 java。
保留私有地址。下面是一个 ES|QL 示例(Linux Elastic Defend 网络事件,最近 7 天):
ini
`
1. FROM logs-endpoint.events.network-*
2. | WHERE @timestamp > NOW() - 7 days
3. AND host.os.type == "linux"
4. AND event.action : "connection_accepted"
5. AND process.name == "java"
6. | STATS
7. accepts = COUNT(*)
8. BY host.name, process.executable, destination.port
9. | SORT accepts DESC
10. | LIMIT 100
`Lobster AI
分诊:这是在 8080/8443 上运行的 Tomcat/JBoss,还是一个无法解释的监听端口?SocketAppender 历史上的默认 TCP 端口是 4560。这个端口只是一个提示,可以用来进一步_检查_,不能证明使用了 Java 序列化(JSON/XML 布局同样会使用 socket)。值得从相同数据中提取并检查的命令行包括:
-
TcpSocketServer -
createSerializedSocketServer -
SerializedLayout -
log4j-server -
ObjectInputStreamLogEventBridge -
FilteredObjectInputStream(很少直接出现在命令行中,更可能出现在源代码或 fat JAR 中)
在磁盘上,搜索应用程序代码仓库和配置中的 SerializedLayout 以及 new FilteredObjectInputStream。如果仍然需要通过网络采集日志,优先使用 JSON 或 Syslog 接收器。
Kibana ES|QL 搜索查询:针对 java 的 connection_accepted 网络事件,按主机和 destination.port 分组
搜索没有 JNDI 前置活动的 Java 派生 shell
目标:检测类似 gadget 的执行行为。
sql
`
1. FROM logs-endpoint.events.process-*
2. | WHERE @timestamp > NOW() - 30 days
3. AND host.os.type == "linux"
4. AND event.action : "exec"
5. AND process.parent.name == "java"
6. AND (
7. process.name IN (
8. "bash", "dash", "sh", "ash", "zsh", "ksh", "fish", "csh", "tcsh", "mksh", "busybox",
9. "curl", "wget", "perl*", "python*", "ruby*", "php*", "lua*", "socat",
10. "nc", "ncat", "netcat", "netcat.openbsd", "netcat.traditional", "nc.openbsd",
11. "nc.traditional", "nohup", "setsid", "disown", "hostname", "whoami", "id"
13. ) OR
14. process.name LIKE "python*" OR
15. process.name LIKE "perl*" OR
16. process.name LIKE "ruby*" OR
17. process.name LIKE "lua*" OR
18. process.name LIKE "php*"
19. )
20. | STATS
21. execs = COUNT(*)
22. BY host.name, process.parent.executable, process.parent.command_line, process.command_line
23. | WHERE execs <= 20
24. | SORT execs ASC
25. | LIMIT 100
`Lobster AI
Kibana ES|QL 查询:检测 Java 父进程在 Log4j 反序列化利用过程中派生 /bin/sh
将每个命中结果通过 parent.entity_id 字段关联到网络事件。如果你在一个非预期端口(389、1389、1099、53、5353)上看到 connection_accepted,并且同一分钟内没有出站 destination.port,那么这就不是 JNDI 序列。检查该 JVM 的类路径中是否存在可用于反序列化的 gadget,以及该进程是否读取 ObjectInputStream。
对于实时主机,可以使用同一个反向/绑定 shell 搜索中的 Osquery:
css
`
1. SELECT p.pid, p.cmdline, lp.port, lp.protocol, lp.address
2. FROM processes p
3. JOIN listening_ports lp ON p.pid = lp.pid
4. WHERE p.name = 'java';
`Lobster AI
Windows 主机同样可能运行这些遗留的 Java 代码。字段名称有所不同,但需要回答的问题并没有改变:谁在监听、谁从 java.exe 派生 cmd.exe/powershell.exe,以及该进程是否是一个序列化 LogEvent 接收器。
MITRE ATT&CK 技术与战术
Elastic 使用 MITRE ATT&CK 框架记录威胁针对企业网络时采用的常见战术、技术和过程。
战术
战术代表某项技术或子技术背后的"为什么",即对手的战术目标,也就是执行某项操作的原因。
技术
技术代表对手通过执行某项操作来实现战术目标的方式。
结论
#4255 议题出现后,我们立即对其进行了深入研究。经历过去几年的情况后,任何将"Log4j"和远程代码执行联系在一起的漏洞都值得立即关注。在通过 PoC 验证确认 Log4j 2.26.1 中确实存在该绕过,并进一步梳理其利用前提后,我们得出的结论是:这不是另一个 Log4Shell。
无论如何,我们分享的检测规则可以检测这一类漏洞利用发生后的活动。通常情况下,Java 进程会在建立连接后派生一个 shell。这些行为本身都不是 #4255 利用的特征,但在调查过程中,可以通过分诊将它们追溯回具体原因。
梳理你的 LogEvent 接收器,搜索监听异常端口的 Java 进程;如果你仍然反序列化 Java LogEvent 流,可以考虑替代方案,例如在使用 JDK 9 时添加 ObjectInputFilter。不要认为仅依靠 FOIS 就能构成安全边界。
祝搜索顺利!