java.rmi.MarshalledObject 绕过 FilteredObjectInputStream 白名单一个自包含、容器化的实验环境,可端到端复现 Apache log4j2 问题 #4255,针对 JDK 17 上的 官方 Log4j 2.26.1 构件,包含阳性对照、加固后的验证基准以及经过验证的缓解措施。
⚠️ 负责任使用声明
本实验环境包含针对 当前未修补 的 Log4j 问题的 可利用的反序列化 RCE (#4255 在撰写本文时仍处于 OPEN /
waiting-for-maintainer状态;未分配 CVE)。它 完全运行在您自己机器上的一次性 Docker 容器中,除 Maven Central(用于下载官方 jar 包)外不连接任何外部资源——不会接触任何目标。
- 原始报告者(U-Sec / 无界安全)在修复完成前暂不公开其 PoC。这是一个 独立复现,专为防御方验证和检测工程而构建。
- 请不要针对您不拥有或未经明确授权测试的系统运行此实验。
- 这演示的是一个应用条件性 RCE,不是通用的 Log4j RCE(请参阅影响范围)。
Log4j 的 FilteredObjectInputStream(FOIS)是一个基于 resolveClass 的反序列化白名单。
其白名单包含 java.rmi.MarshalledObject。MarshalledObject 将其载荷存储为
不透明的 byte[],而 MarshalledObject.get() 会在一个全新的、未经过滤的
ObjectInputStream 上反序列化该载荷——因此白名单永远不会检查内部对象图。
Log4j 自身会触发这一点:Log4jLogEvent$LogEventProxy(自 2.8 起 LogEvent 的序列化线上形式)
将事件 Message 封装在 MarshalledObject 中,并在反序列化期间自动调用 .get()
(readResolve() → message())。任何通过 FOIS 读取序列化 LogEvent 的应用程序都会对攻击者字节执行
未经过滤的反序列化——并且由于 message() 会吞掉由此产生的异常并回退到 SimpleMessage,
接收方会记录一个良性事件并继续运行。该漏洞利用是无声的。
rel/2.26.1 验证)作为已弃用的 log4j-samples ObjectInputStreamLogEventBridge 的忠实替代品:一个
未认证的 TCP 接收器,通过 FOIS 读取每个连接中的一个序列化 LogEvent。
攻击者发送单个序列化对象;验证基准是一个写入目录的证明文件,该目录
仅绑定挂载到接收器容器中,因此其出现即证明代码通过反序列化在接收器内部执行。
rce = 代码已执行。reject = FOIS 在外部流上抛出异常。silent = 外部对象已处理,
但未执行代码(在更深处被阻止,或 gadget 版本是安全的)。
./run.sh # 或:make run
要求:仅需 Docker(JDK 会以 eclipse-temurin:17-jdk 形式拉取)。脚本会下载
官方 jar 包并在使用前对照 Maven Central SHA-1 进行验证。可使用
LOG4J_VERSION=2.20.0 ./run.sh 固定漏洞范围内的不同 Log4j 版本。
LogEvent TCP 写入。
它不能通过记录字符串来触发(与 Log4Shell 不同)——需要原始序列化字节到达套接字桥。LogEvent → Log4j 的 writeReplace()/writeObject() 将 Message
封装在 MarshalledObject 中 → gadget 隐藏在其不透明的 byte[] 内 → 在接收器端,
readResolve() → message() → MarshalledObject.get() 打开一个全新的未过滤流 →
gadget → Runtime.exec。src/attacker/Attacker.java(poc2)将纯 CommonsCollections6
对象图拼接进 的 ,因此。-Djdk.serialFilter='!java.rmi.MarshalledObject'(S5)。
注意: 这也会阻止合法的序列化 LogEventProxy 对象(它们同样使用
MarshalledObject)——该方法有效,但对序列化日志传输并非透明。maxdepth/maxbytes 过滤器。进程级过滤器会传播到
内部 MarshalledObject 流,且深度在其中重新开始计算,因此 maxdepth=5 可以阻止 CC6(S6),
但浅层 gadget 仍可通过。这取决于链深度,并非边界。MarshalledObject 并将封装的
消息迁移到 Log4j 的过滤式 writeWrappedObject/readWrappedObject。LogEvent 接收器并且其类路径上存在可用的 gadget 版本。
普通 Log4j 部署不会运行此类接收器。net.server.TcpSocketServer
于 2017 年移出 log4j-core;自 2.9.0 起不存在)。现代接收器属于
应用/示例代码,本实验环境正是对其建模。uid=0 是容器根用户——不存在 Docker 逃逸;RCE 以接收器进程身份运行。MarshalledObject 绕过 resolveClass 过滤器)是已知的先前技术;
请参阅 Apache 讨论 #4168
("Log4j 2.x 反序列化加固")。Log4j 特有的自动触发机制是 #4255 的贡献。run.sh 便携式运行器(校验和固定、加固验证基准)
Makefile make build | run | clean
src/victim/Receiver.java FOIS 日志接收器(ObjectInputStreamLogEventBridge 替代品)
src/attacker/Attacker.java 载荷构建器:对照、PoC-1、PoC-2(CC6 + 字节拼接)
src/attacker/EvilMessage.java PoC-1 自包含 gadget
docs/RESULTS.md 证据矩阵 + 分析
漏洞由 U-Sec(无界安全) 在 Apache log4j2 #4255 中报告。本仓库是一个 独立的复现/验证实验环境,用于防御研究和检测工程。
| # | 位置 | 缺陷 |
|---|
| 1 | log4j-api …/util/internal/SerializationUtil.java | REQUIRED_JAVA_CLASSES 包含 java.rmi.MarshalledObject |
| 2 | log4j-api …/util/FilteredObjectInputStream.java | 仅重写了 resolveClass();MarshalledObject 的 objBytes 载荷对其不可见 |
| 3 | log4j-core …/impl/Log4jLogEvent.java | LogEventProxy.marshalledMessage 是一个 MarshalledObject<Message> |
| 4 | log4j-core …/impl/Log4jLogEvent.java | message() 调用 marshalledMessage.get()(无过滤)并吞掉所有异常 |
| # | 场景 | 受害者类路径 | jdk.serialFilter | 预期结果 |
|---|
| S1 | 不允许的 gadget 顶层发送 | + gadget | 无 | 拒绝 — FOIS 强制执行其白名单 |
| S2 | 相同 gadget 封装在 LogEvent 中 | + gadget | 无 | rce — 自动触发(受害者上需要 gadget 类) |
| S3 | 原始 CommonsCollections6 顶层 | log4j + cc-3.2.1 | 无 | 拒绝 — FOIS 阻止 CC |
| S4 | CC6 拼接进 MarshalledObject | 仅 log4j + cc-3.2.1 | 无 | rce — 受害者上无需攻击者类 |
| S5 | S4 载荷 | log4j + cc-3.2.1 | !java.rmi.MarshalledObject | 拒绝 — 缓解措施 |
| S6 | S4 载荷 | log4j + cc-3.2.1 | maxdepth=5;maxbytes=1000000 | 无声 — 内部流继承过滤器;CC6 过深 |
| S7 | S4 载荷 | log4j + cc-3.2.2 | 无 | 无声 — 3.2.2 禁用不安全的 functor 反序列化 |
MarshalledObjectobjBytes