An upstream report raised an alarming possibility: a class filter around Log4j2’s Java-serialized event receiver could approve an outer event and still lose control of what was deserialized inside it. The report then disappeared before final vendor guidance was available, leaving two bad options: dismiss an unverified claim, or repeat the phrase “Log4j RCE” without knowing what it actually applied to.
We chose a third option. Pruva reconstructed the boundary, exercised the real Apache sample TCP receiver, and ran the same network input against vulnerable and controlled targets. The result was receiver-side command execution in two fresh JVMs. It was also much narrower than the phrase “the Log4Shell” suggests.
No CVE has been assigned, and no vendor-fixed release is public at this update.
The verified reproduction is now public. A separate containerized PoC was published after our August 26, 2026 evidence freeze and exposes the same boundary with a standard Commons Collections chain. Pruva’s runtime proof was already complete and frozen before that repository existed. We cite the later PoC only as post-freeze corroboration and as a public reference for mechanics that were previously withheld.
The receiver path we tested does not parse ordinary log text. It accepts Java-serialized Log4j events over TCP. Apache’s sample server wraps the socket in ObjectInputStreamLogEventBridge , which uses Log4j’s FilteredObjectInputStream (FOIS) before calling readObject() .
FOIS is a real boundary. Our direct control sent a non-allowlisted object through the same bridge, and the receiver rejected it. Any explanation that starts with “the filter simply does nothing” is wrong.
The problem appears one transition later. A legitimate serialized Log4jLogEvent uses LogEventProxy during serialization. In the tested Log4j build, that proxy can carry message content in a java.rmi.MarshalledObject . The outer stream sees an allowlisted event proxy and an allowed Java carrier. The carrier’s nested object graph is still opaque bytes at that point.
When the event proxy reconstructs the message, the carrier unpacks those bytes through another object input stream. That second stream is not the FOIS instance that approved the outer class descriptors. The original class decision is therefore not transitive across the nested deserialization boundary.
That distinction is the useful new information. The vulnerable condition is not “filter absent.” It is “filter applied to one stream, then security-sensitive deserialization resumed in another.”
The serialized wire object is not a normal Log4jLogEvent . Java serialization calls the event’s replacement logic and emits Log4jLogEvent.LogEventProxy . One of that proxy’s fields is a MarshalledObject . Log4j places java.rmi.MarshalledObject in its required Java class set because it is the message delegate used by the event’s serialized form.
That choice creates two different views of the same input:
The public PoC makes the separation concrete. It first serializes a benign event proxy, locates the serialized bytes of the benign inner SimpleMessage , and replaces that byte array with a serialized CommonsCollections6 graph while correcting the array length. The receiver therefore does not need an attacker-defined class. It needs only the Log4j classes required for the event and a compatible gadget library already on its classpath.
On reconstruction, LogEventProxy.readResolve() calls its message recovery path. That path invokes MarshalledObject.get() , which opens a MarshalledObjectInputStream over the embedded bytes. The outer FOIS decision cannot inspect the graph before this point, and it is not the stream resolving the inner graph. With Commons Collections 3.2.1 present, the published graph links TiedMapEntry , LazyMap , and a ChainedTransformer to process execution. Commons Collections 3.2.2 is an important negative control because it disables unsafe functor deserialization by default.
The byte-splicing technique is one way to construct the carrier, not the root cause. Pruva’s proof used a receiver-loadable message wrapper to carry a CC6 graph and then tested the same network bytes against every target and control. Both constructions reach the same security transition: the inner graph is resolved only after the outer filter has admitted the carrier.
At the August 26, 2026 evidence freeze, the public record did not establish four facts that change how this issue should be evaluated:
Together, those observations turn a source-level concern into a bounded production-path result. They also explain why broad Log4j inventory counts are not an exposure measurement: the receiver, nested transition, classpath, reachability, and runtime filtering all matter.
The direct-rejection and nested-capability tiers used purpose-built control inputs. The final command-execution input was then replayed unchanged against the vulnerable, source-control, and JEP 290 receivers:
A local program that calls ObjectInputStream.readObject() on attacker-provided bytes would prove very little a network-facing Log4j deployment. Our acceptance contract required the actual serialized receiver path:
The target used the Apache logging-log4j-samples server at a fixed source commit and official Log4j API/Core 2.26.1 artifacts. Each attempt started a fresh receiver JVM on a randomized port. The harness separately checked service readiness, connection acceptance, the direct filter control, nested deserialization, and the final process-side effect.
One exact final input was generated once and replayed unchanged. It produced run-unique command markers in two fresh vulnerable receivers with different process identities. The same bytes were then rejected by two fresh receivers using a narrow Log4j source-level control, with no marker. A separate run configured JEP 290, the JDK Object Input Filter mechanism, to reject java.rmi.MarshalledObject ; it rejected the unchanged input and produced no marker.
The full reproduction contains 29 digest-bound proof artifacts covering the direct FOIS control, two fresh nested-capability receivers, two fresh command-execution receivers, two fresh narrow-control receivers, and one JEP 290 mitigation receiver. The public record includes the runnable script, source, target identity, receiver logs, exact-input identity, runtime manifest, and typed verdict.
The independently published lab reaches the same primitive with an approximately 2.8 KB serialized event and a faithful receiver stand-in. In addition to that later stand-in result, Pruva’s frozen product-path proof shows the unchanged input traversing Apache’s sample TcpSocketServer and ObjectInputStreamLogEventBridge with repeated vulnerable and defensive controls.
Log4Shell became broadly exploitable because attacker-controlled text could reach lookup behavior through ordinary logging paths in a huge number of applications. The path we verified has a very different exposure model.
An application is in the demonstrated risk envelope only when all of these conditions hold:
The tested receiver had Commons Collections 3.2.1 and a receiver-loadable message wrapper, custom code that is not a Log4j default, so we could measure the worst case for that classpath. Neither is a default Log4j dependency, and their presence in our lab must not be generalized to every Log4j application. The public byte-splicing PoC removes the custom-message precondition, but it does not remove the need for a usable gadget surface on the receiver.
Ordinary calls such as logger.info(attackerText) do not enter the receiver path we tested. Merely finding log4j-core in a software inventory does not prove reachability. Internet scanning for a single familiar Log4j port would also be misleading because operators can bind serialized receivers to arbitrary ports or embed similar bridge code elsewhere.
Where all five preconditions hold, the impact is serious: our proof reached command execution as the receiver process account from an unauthenticated TCP peer. We did not test or claim privilege escalation.
The unchanged-input controls matter more than a successful marker. The narrow source control removed java.rmi.MarshalledObject from Log4j’s required Java class set, causing the outer filter to reject the carrier before event reconstruction. A JEP 290 policy separately rejected the unchanged input. Neither control produced the command-side effect.
This gives defenders useful actions before a final vendor advisory is available:
The process tree is a stronger detection signal than the serialized byte pattern. In the confirmed case, the network-facing receiver JVM becomes the parent of an unexpected operating-system process. Defenders should correlate that child process with a connection to the serialized-event listener, the receiver’s Java command line, and object-input-filter rejection telemetry. A receiver can log a deserialization error or continue handling later input after the nested graph has already caused a side effect, so an exception alone is not evidence that execution was prevented.
These are defense-in-depth measures, not a substitute for the vendor’s final remediation. Apache’s Log4j 2.x deserialization-hardening discussion describes MarshalledObject.get() as an allow-list escape but also states that deserializing untrusted Log4j data is outside the project’s security guarantee. The old in-core serialized socket server was removed after 2.8.2; modern exposure comes from applications, copied sample code, or legacy integrations that still feed serialized events into this path.
Pruva executed Log4j API/Core 2.26.1. Source history places the relevant API allow-list and Core event-proxy machinery across a wider range, but that is not the same as proving every release or deployment remotely exploitable. No CVE, vendor severity, final affected matrix, or vendor-fixed release is public at this update, so we do not invent one. Our narrow source change identifies the first divergent boundary; it is not presented as Apache’s final patch.
The exploit mechanics, a reusable public PoC, and Pruva’s production-path reproduction are now available. Publishing the evidence does not turn the conditional receiver path into a default Log4j exposure. Operators still need to establish listener reachability, serialized-event usage, the exact Log4j path, the receiver classpath, and effective JVM filters.
The unresolved questions are governance and remediation questions: whether Apache will assign or request an identifier, what source change it will ship, how it will describe affected legacy integrations, and which release will carry that change. The Apache discussion currently treats the condition as hardening because applications are not supposed to deserialize untrusted event data. That threat-model position and the runtime result can both be true: the default product may not expose an untrusted receiver, while an application that does expose one can cross the filter and execute a receiver-loadable graph.
The most reusable conclusion is precise and limited: a filter that correctly constrains one object stream does not protect a second stream opened later during object reconstruction. Security review has to follow the object graph across that transition, not stop when the outer stream returns an allowed class name.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
