别再来一个 Log4Shell:深入解析 Log4j 2 反序列化允许列表绕过 📅 发布时间:2026/8/30 5:15:14 👁 浏览次数: 作者来自 Elastic Ruben Groenewoud, Terrance DeJesus, Bryan Porras Blanch, Eric Forte这不是另一个 Log4Shell。Log4ShellCVE-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.1JAR 包上进行的实验。我们并不声称能够完整检测所有自定义反序列化器。这个 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-coreorg.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-44228Log4j2的利用》 则介绍了 Log4Shell。一位 Log4j 贡献者已经在 讨论 #4168 的发现 1中记录了相同的允许列表逃逸方式。该讨论发表于 2026 年 7 月 1 日MarshalledObject.get()会在没有 Log4j 过滤器的情况下进行反序列化。该讨论将这项工作定位为 2.x 的安全加固。之后Issue #4255 对相同路径进行了命名。Java 反序列化允许列表绕过是如何工作的这里的每一个组件单独来看都不是漏洞。这个绕过问题来自官方2.26.1源代码中的三个行为。将它们串联起来后攻击者控制的对象图就可以在没有应用任何过滤器的情况下完成反序列化。具体如下步骤组件行为为什么重要1SerializationUtil.REQUIRED_JAVA_CLASSESjava.rmi.MarshalledObject作为 Message 委托对象位于允许列表中该承载类按设计就是允许的2FilteredObjectInputStream.resolveClass()只检查它所读取的流中存在的类描述符作为不透明byte[]保存的负载永远不会被检查3Log4jLogEvent.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()只能看到该流中的类描述符。protected Class? resolveClass(final ObjectStreamClass desc) throws IOException, ClassNotFoundException { final String name SerializationUtil.stripArray(desc.getName()); if (!(isAllowedByDefault(name) || allowedExtraClasses.contains(name))) { throw new InvalidObjectException(Class is not allowed for deserialization: name); } return super.resolveClass(desc); }来源FilteredObjectInputStream.java位于 rel/2.26.1。MarshalledObject将其负载作为不透明的byte[]携带。这些内部类名称不会经过这个resolveClass()。3.LogEventProxy随后调用MarshalledObject.get()。Log4jLogEvent.LogEventProxy从2.8开始就一直持有private MarshalledObjectMessage marshalledMessage。外层流接受代理对象后readResolve()会构建一个Log4jLogEvent而message()会执行以下操作private Message message() { if (marshalledMessage ! null) { try { return marshalledMessage.get(); } catch (final Exception ex) { // ignore me } } return new SimpleMessage(messageString); }来源Log4jLogEvent.java位于 rel/2.26.1。get()抛出的任何异常都会被处理。接收端仍然可能看起来运行正常并回退到SimpleMessage(messageString)。MarshalledObject.get()会在内部的ObjectInputStream上对objBytes进行反序列化而这个流不是FilteredObjectInputStream。在 JDK 9 及更高版本中MarshalledObject可以复制外层流的 JEP 290ObjectInputFilter。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-api2.26.1 和log4j-core2.26.1。接收端实现了公开的 FOIS 加LogEvent读取逻辑并在测试期间绑定到回环地址。一个FilteredObjectInputStream会在外层流中拒绝的类在被放入序列化的Log4jLogEvent中的MarshalledObject携带后成功执行了一次。在该实验环境中要实现命令执行需要同时具备 Java 序列化的LogEvent接收端以及已经存在于受害者 JVM 中的 gadget 库。仅有log4j-api和log4j-core并不足以实现命令执行。在同一个 FOIS 实例上安装 JEP 290ObjectInputFilter后我们使用的负载被拒绝。在调用该方法之前流过滤器为null。展示 Log4j 反序列化命令执行所需条件的表格序列化的 LogEvent 接收端和 gadget 库受影响的 Log4j 2 版本我们检查了多个 JAR 版本以确认log4j-api中是否同时存在该字段和 FOIS。检查的版本marshalledMessage字段log4j-api中的FilteredObjectInputStream2.6.2、2.7不存在不存在2.8存在不存在2.8.2存在存在log4j-core2.9.1、2.10.0存在不存在2.11.0至2.26.1存在存在log4j-api该绕过需要同时存在marshalledMessage字段和FilteredObjectInputStream后者必须是被绕过的组件。这两个条件在2.8.2FOIS 位于log4j-core以及2.11.0至2.26.1FOIS 位于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或者任何自定义的FilteredObjectInputStreamLogEvent桥接程序。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例如 TomcatBootstrap、Jetty、WildFly、Spring Boot loader、Jenkins.war等。独立运行的 FOIS 采集器通常不在这个列表中。我们建议在面向 Internet 的主机上运行以下规则Potential Reverse Shell via JavaSuspicious Child Execution via Web ServerUnusual Child Execution via Web ServerSuspicious Command Execution via Web ServerUnusual Command Execution via Web ServerPotential Java Service Exploitation via Suspicious Child ProcessShell Execution via Java Parent ProcessUnusual File Creation by Web Server如果你看到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 天FROM logs-endpoint.events.network-* | WHERE timestamp NOW() - 7 days AND host.os.type linux AND event.action : connection_accepted AND process.name java | STATS accepts COUNT(*) BY host.name, process.executable, destination.port | SORT accepts DESC | LIMIT 100分诊这是在8080/8443上运行的 Tomcat/JBoss还是一个无法解释的监听端口SocketAppender历史上的默认 TCP 端口是4560。这个端口只是一个提示可以用来进一步检查不能证明使用了 Java 序列化JSON/XML 布局同样会使用 socket。值得从相同数据中提取并检查的命令行包括TcpSocketServercreateSerializedSocketServerSerializedLayoutlog4j-serverObjectInputStreamLogEventBridgeFilteredObjectInputStream很少直接出现在命令行中更可能出现在源代码或 fat JAR 中在磁盘上搜索应用程序代码仓库和配置中的SerializedLayout以及new FilteredObjectInputStream。如果仍然需要通过网络采集日志优先使用 JSON 或 Syslog 接收器。Kibana ES|QL 搜索查询针对 java 的 connection_accepted 网络事件按主机和 destination.port 分组搜索没有 JNDI 前置活动的 Java 派生 shell目标检测类似 gadget 的执行行为。FROM logs-endpoint.events.process-* | WHERE timestamp NOW() - 30 days AND host.os.type linux AND event.action : exec AND process.parent.name java AND ( process.name IN ( bash, dash, sh, ash, zsh, ksh, fish, csh, tcsh, mksh, busybox, curl, wget, perl*, python*, ruby*, php*, lua*, socat, nc, ncat, netcat, netcat.openbsd, netcat.traditional, nc.openbsd, nc.traditional, nohup, setsid, disown, hostname, whoami, id ) OR process.name LIKE python* OR process.name LIKE perl* OR process.name LIKE ruby* OR process.name LIKE lua* OR process.name LIKE php* ) | STATS execs COUNT(*) BY host.name, process.parent.executable, process.parent.command_line, process.command_line | WHERE execs 20 | SORT execs ASC | LIMIT 100Kibana ES|QL 查询检测 Java 父进程在 Log4j 反序列化利用过程中派生 /bin/sh将每个命中结果通过parent.entity_id字段关联到网络事件。如果你在一个非预期端口389、1389、1099、53、5353上看到connection_accepted并且同一分钟内没有出站destination.port那么这就不是 JNDI 序列。检查该 JVM 的类路径中是否存在可用于反序列化的 gadget以及该进程是否读取ObjectInputStream。对于实时主机可以使用同一个反向/绑定 shell 搜索中的 OsquerySELECT p.pid, p.cmdline, lp.port, lp.protocol, lp.address FROM processes p JOIN listening_ports lp ON p.pid lp.pid WHERE p.name java;Windows 主机同样可能运行这些遗留的 Java 代码。字段名称有所不同但需要回答的问题并没有改变谁在监听、谁从java.exe派生cmd.exe/powershell.exe以及该进程是否是一个序列化LogEvent接收器。MITRE ATTCK 技术与战术Elastic 使用 MITRE ATTCK 框架记录威胁针对企业网络时采用的常见战术、技术和过程。战术战术代表某项技术或子技术背后的“为什么”即对手的战术目标也就是执行某项操作的原因。初始访问Initial Access执行Execution横向移动Lateral Movement技术技术代表对手通过执行某项操作来实现战术目标的方式。利用面向公众的应用程序Exploit Public-Facing Application命令和脚本解释器Unix ShellCommand and Scripting Interpreter: Unix Shell利用远程服务Exploitation of Remote Services结论#4255 议题出现后我们立即对其进行了深入研究。经历过去几年的情况后任何将“Log4j”和远程代码执行联系在一起的漏洞都值得立即关注。在通过 PoC 验证确认 Log4j2.26.1中确实存在该绕过并进一步梳理其利用前提后我们得出的结论是这不是另一个 Log4Shell。无论如何我们分享的检测规则可以检测这一类漏洞利用发生后的活动。通常情况下Java 进程会在建立连接后派生一个 shell。这些行为本身都不是 #4255 利用的特征但在调查过程中可以通过分诊将它们追溯回具体原因。梳理你的LogEvent接收器搜索监听异常端口的 Java 进程如果你仍然反序列化 JavaLogEvent流可以考虑替代方案例如在使用 JDK 9 时添加ObjectInputFilter。不要认为仅依靠 FOIS 就能构成安全边界。祝搜索顺利原文https://www.elastic.co/security-labs/threat-command/java-deserialization-vulnerability-log4j-2