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]

Reply via email to