Description
Reported by U-Sec (Wujie Security). Public disclosure of security vulnerabilities is not enabled on this repository, so we are reporting this through the issue tracker. If you prefer, this issue can be converted into a private discussion / advisory; we will keep full details confidential and are happy to move this report to security@logging.apache.org on request — please just confirm receipt and a tracking case number so we can coordinate.
FilteredObjectInputStream explicitly allows java.rmi.MarshalledObject in its default allowlist. A MarshalledObject carries its payload as an opaque byte[] (objBytes), so a resolveClass-based filter never inspects it. When the payload is later unwrapped, MarshalledObject.get() constructs a new plain ObjectInputStream on those bytes and deserializes it completely unfiltered.
Log4j provides the trigger itself: Log4jLogEvent.LogEventProxy serializes the event Message inside a MarshalledObject (field marshalledMessage, since 2.8) and calls marshalledMessage.get() inside readResolve() → message(). A single serialized LogEventProxy received by any FOIS-based log receiver therefore executes attacker-controlled deserialization with zero filtering, while the receiver continues processing normally — the ClassCastException from a non-Message payload is swallowed by message(), which falls back to SimpleMessage(messageString).
This means the complete Java serialization gadget catalog applies: the payload does not need to implement Message and does not need to pass the allowlist.
Configuration
Version: log4j-api 2.11.0 – 2.26.1 / log4j-core 2.8.0 – 2.26.1 (verified against official 2.26.1 artifacts from Maven Central)
Operating system: platform-independent
JDK: all versions (Java 8: no filter API at all; JDK 9+: MarshalledObject copies the stream's ObjectInputFilter, but FOIS never calls setObjectInputFilter() — the stream filter is null, so the capture is a no-op)
Root cause (rel/2.26.1)
| # |
Location |
Defect |
| 1 |
log4j-api …/util/internal/SerializationUtil.java:81 |
REQUIRED_JAVA_CLASSES contains java.rmi.MarshalledObject |
| 2 |
log4j-api …/util/FilteredObjectInputStream.java:66-72 |
resolveClass() only sees top-level descriptors; objBytes payload is invisible |
| 3 |
log4j-core …/impl/Log4jLogEvent.java:1134 |
private MarshalledObject<Message> marshalledMessage; |
| 4 |
log4j-core …/impl/Log4jLogEvent.java:1265-1274 |
message() calls marshalledMessage.get() (filterless) and swallows all exceptions |
private Message message() {
if (marshalledMessage != null) {
try {
return marshalledMessage.get(); // NEW plain ObjectInputStream, NO filter
} catch (final Exception ex) { /* ignore me */ }
}
return new SimpleMessage(messageString);
}
Logs
Verified end-to-end against official 2.26.1 jars, receiver semantics byte-for-byte from apache/logging-log4j-samples log4j-server / ObjectInputStreamLogEventBridge:
try (ObjectInputStream in = new FilteredObjectInputStream(socket.getInputStream())) {
final LogEvent event = (LogEvent) in.readObject();
}
Control test (malicious object sent directly, no wrapper) — correctly rejected:
InvalidObjectException: Class is not allowed for deserialization: poc.EvilMessage
Attack test (same object wrapped via LogEventProxy → MarshalledObject):
[receiver] bound to 0.0.0.0 (all interfaces), no auth, port 4563
[receiver] connection from /<attacker-address>
[attacker] single fire-and-forget write: 1356 bytes - done
[!!] GadgetOnly.readObject() EXECUTED - no Message iface, no allowlist entry
[receiver] processed event: benign message
A non-Message, non-allowlisted object executes inside the receiver; the receiver shows no error and keeps processing. A second PoC demonstrates the collection-trigger shape (CC6/CC7/CB-style hashCode() during HashSet.readObject) with the receiver dialing an outbound TCP connection back to the attacker — effective when only inbound traffic is firewalled.
Reproduction
Full PoC sources (JUnit-style standalone runners, no external dependencies beyond the official jars) are available to maintainers on request and will be published after a fix is released. Minimal attacker-side sketch:
final Log4jLogEvent event = Log4jLogEvent.newBuilder()
.setLoggerName("attacker")
.setLevel(Level.INFO)
.setLoggerFqcn("x")
.setMessage(new MessageShell(gadget)) // Message wrapper, arbitrary graph inside
.build();
final LogEventProxy proxy = new LogEventProxy(event, false);
// serialize proxy, write once to any FOIS receiver socket — done
Impact
- Remote code execution on any service that receives serialized
LogEvents through FOIS (official samples ObjectInputStreamLogEventBridge, vertigo-analytics-server createSerializedSocketServer — FOIS bridge by default, no TLS/auth, binds all interfaces — and ≤ 2.14.x deployments where the receiver lived inside log4j-core as net.server.TcpSocketServer.createSerializedSocketServer).
- Without a gadget library on the classpath: resource-exhaustion DoS (object-graph bombs) and injection of attacker-chosen log content into downstream appenders.
- We note the inner stream in
SerializationUtil.readWrappedObject() (used by ObjectMessage) is filtered — the unprotected path is specifically LogEventProxy.marshalledMessage.
Suggested fix
Option A (minimal, two files): remove java.rmi.MarshalledObject from REQUIRED_JAVA_CLASSES; transport the marshalled message via SerializationUtil.writeWrappedObject() / readWrappedObject() (plain byte[] + inner FOIS), the approach already used by ObjectMessage. Removes the unfiltered get() entirely; covers Java 8.
Option B (structural): migrate FOIS to a JEP 290 ObjectInputFilter, which sees through MarshalledObject on JDK 9+.
We have read the published threat model, FAQ and disclosure records and did not find this issue reported. We acknowledge FOIS is positioned as defense-in-depth; we respectfully submit that this case differs from operator misuse: the trigger is log4j-core's own serialization format (LogEventProxy) automatically unwrapping the payload with an unfiltered stream — fixable inside log4j, and the pattern affects every resolveClass-based allowlist that permits java.*. We request a tracking case number and will keep details confidential until a fix is released (or 90 days after acknowledgment).
Credits: U-Sec (Wujie Security)
Description
Reported by U-Sec (Wujie Security). Public disclosure of security vulnerabilities is not enabled on this repository, so we are reporting this through the issue tracker. If you prefer, this issue can be converted into a private discussion / advisory; we will keep full details confidential and are happy to move this report to security@logging.apache.org on request — please just confirm receipt and a tracking case number so we can coordinate.
FilteredObjectInputStreamexplicitly allowsjava.rmi.MarshalledObjectin its default allowlist. AMarshalledObjectcarries its payload as an opaquebyte[](objBytes), so aresolveClass-based filter never inspects it. When the payload is later unwrapped,MarshalledObject.get()constructs a new plainObjectInputStreamon those bytes and deserializes it completely unfiltered.Log4j provides the trigger itself:
Log4jLogEvent.LogEventProxyserializes the eventMessageinside aMarshalledObject(fieldmarshalledMessage, since 2.8) and callsmarshalledMessage.get()insidereadResolve()→message(). A single serializedLogEventProxyreceived by any FOIS-based log receiver therefore executes attacker-controlled deserialization with zero filtering, while the receiver continues processing normally — theClassCastExceptionfrom a non-Messagepayload is swallowed bymessage(), which falls back toSimpleMessage(messageString).This means the complete Java serialization gadget catalog applies: the payload does not need to implement
Messageand does not need to pass the allowlist.Configuration
Version: log4j-api 2.11.0 – 2.26.1 / log4j-core 2.8.0 – 2.26.1 (verified against official 2.26.1 artifacts from Maven Central)
Operating system: platform-independent
JDK: all versions (Java 8: no filter API at all; JDK 9+:
MarshalledObjectcopies the stream'sObjectInputFilter, but FOIS never callssetObjectInputFilter()— the stream filter isnull, so the capture is a no-op)Root cause (rel/2.26.1)
log4j-api …/util/internal/SerializationUtil.java:81REQUIRED_JAVA_CLASSEScontainsjava.rmi.MarshalledObjectlog4j-api …/util/FilteredObjectInputStream.java:66-72resolveClass()only sees top-level descriptors;objBytespayload is invisiblelog4j-core …/impl/Log4jLogEvent.java:1134private MarshalledObject<Message> marshalledMessage;log4j-core …/impl/Log4jLogEvent.java:1265-1274message()callsmarshalledMessage.get()(filterless) and swallows all exceptionsLogs
Verified end-to-end against official 2.26.1 jars, receiver semantics byte-for-byte from
apache/logging-log4j-sampleslog4j-server/ObjectInputStreamLogEventBridge:Control test (malicious object sent directly, no wrapper) — correctly rejected:
Attack test (same object wrapped via
LogEventProxy→MarshalledObject):A non-
Message, non-allowlisted object executes inside the receiver; the receiver shows no error and keeps processing. A second PoC demonstrates the collection-trigger shape (CC6/CC7/CB-stylehashCode()duringHashSet.readObject) with the receiver dialing an outbound TCP connection back to the attacker — effective when only inbound traffic is firewalled.Reproduction
Full PoC sources (JUnit-style standalone runners, no external dependencies beyond the official jars) are available to maintainers on request and will be published after a fix is released. Minimal attacker-side sketch:
Impact
LogEventsthrough FOIS (official samplesObjectInputStreamLogEventBridge, vertigo-analytics-servercreateSerializedSocketServer— FOIS bridge by default, no TLS/auth, binds all interfaces — and ≤ 2.14.x deployments where the receiver lived inside log4j-core asnet.server.TcpSocketServer.createSerializedSocketServer).SerializationUtil.readWrappedObject()(used byObjectMessage) is filtered — the unprotected path is specificallyLogEventProxy.marshalledMessage.Suggested fix
Option A (minimal, two files): remove
java.rmi.MarshalledObjectfromREQUIRED_JAVA_CLASSES; transport the marshalled message viaSerializationUtil.writeWrappedObject()/readWrappedObject()(plainbyte[]+ inner FOIS), the approach already used byObjectMessage. Removes the unfilteredget()entirely; covers Java 8.Option B (structural): migrate FOIS to a JEP 290
ObjectInputFilter, which sees throughMarshalledObjecton JDK 9+.We have read the published threat model, FAQ and disclosure records and did not find this issue reported. We acknowledge FOIS is positioned as defense-in-depth; we respectfully submit that this case differs from operator misuse: the trigger is log4j-core's own serialization format (
LogEventProxy) automatically unwrapping the payload with an unfiltered stream — fixable inside log4j, and the pattern affects everyresolveClass-based allowlist that permitsjava.*. We request a tracking case number and will keep details confidential until a fix is released (or 90 days after acknowledgment).Credits: U-Sec (Wujie Security)