U-Sec opened a new issue, #4255: URL: https://github.com/apache/logging-log4j2/issues/4255
## 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 [email protected] 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 | ```java 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`: ```java 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: ```java 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)* -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
