# REPRO-2026-00338: Apache Log4j2 serialized LogEvent filter bypass to conditional RCE ## Summary Status: published Severity: critical CVSS: Unknown CWE: CWE-502 (Deserialization of Untrusted Data) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00338 GHSA: GHSA-LOG4J2-4255-MARSHALLEDOBJECT ## Package Name: org.apache.logging.log4j:log4j-core Ecosystem: maven Affected: log4j-api 2.11.0-2.26.1; log4j-core 2.8.0-2.26.1 (verified against official 2.26.1 jars from Maven Central) Fixed: none yet - upstream issue open, waiting-for-maintainer label; no CVE/GHSA assigned ## Root Cause # Root Cause Analysis ## Summary Apache Log4j2 2.26.1 and earlier permits an allowlist bypass in `FilteredObjectInputStream` (FOIS) because `java.rmi.MarshalledObject` is itself allowlisted. A serialized `Log4jLogEvent.LogEventProxy` can carry attacker-controlled inner serialized bytes that are automatically unpacked by `MarshalledObject.get()` through another `ObjectInputStream`. In this run, one exact payload was replayed through the real Apache `TcpSocketServer` / `ObjectInputStreamLogEventBridge` TCP product path and executed an attacker-selected OS command in two fresh receiver JVMs. The same bytes were rejected by two receivers using the minimal patched API. ## Impact - **Affected components:** `log4j-api` 2.11.0 through 2.26.1 and `log4j-core` 2.8.0 through 2.26.1 when used by a FOIS-based serialized-event receiver. - **Validated product path:** `org.apache.logging.log4j.server.TcpSocketServer` with `ObjectInputStreamLogEventBridge` from Apache `logging-log4j-samples` commit `672a1555c7f5670e7affcc7b9984a90b492eb322`, running official Log4j API/Core 2.26.1 artifacts. - **Risk:** Critical where this unauthenticated serialized-event listener is reachable and a usable gadget library is on the receiver classpath. An attacker can bypass the intended class allowlist and execute commands with the receiver process account. ## Impact Parity - **Disclosed/claimed maximum impact:** Remote unauthenticated arbitrary deserialization leading to code execution. - **Reproduced impact from this run:** Remote receiver-side command execution through the required TCP entrypoint. A single generated CC6 payload file was replayed unchanged to two vulnerable JVM instances and produced process-unique `PWNED-TIER2` markers containing receiver-side shell and parent process IDs plus account identity. - **Parity:** `full`. - **Not demonstrated:** Privilege escalation beyond the account running the receiver was not claimed or tested. ## Root Cause Vulnerable `log4j-api` includes `java.rmi.MarshalledObject` in `SerializationUtil.REQUIRED_JAVA_CLASSES`. FOIS checks outer class descriptors in `resolveClass()`, but the object graph stored by `MarshalledObject` is opaque bytes at that stage. During deserialization of `Log4jLogEvent.LogEventProxy`, `readResolve()` calls `message()`, which calls `marshalledMessage.get()`. That operation deserializes the inner graph in another `ObjectInputStream`, outside FOIS's class-name check. A non-allowlisted object can therefore execute its `readObject()` callback, and a receiver-loadable gadget graph can reach `Runtime.exec`. The tested minimal correction removes `java.rmi.MarshalledObject` from `REQUIRED_JAVA_CLASSES`, causing FOIS to reject the carrier before its inner bytes are unpacked. `bundle/logs/fix.patch` records the exact change. The upstream report is https://github.com/apache/logging-log4j2/issues/4255; it was still open at reproduction time and did not identify a merged fix commit, so the negative control applies the report's minimal suggested fix to official 2.26.1 source. ## Reproduction Steps 1. Run `bash bundle/repro/reproduction_steps.sh` from any directory. The script uses `PRUVA_ROOT` or resolves the bundle path itself. 2. It reads `bundle/project_cache_context.json`, uses `/repo` when prepared, verifies the Apache samples origin and exact commit, and extracts the real `log4j-server` sources. 3. It verifies official Log4j 2.26.1 and Commons Collections 3.2.1 jar hashes, compiles the real `TcpSocketServer` and `ObjectInputStreamLogEventBridge` classes, and records their runtime `CodeSource` paths. 4. It generates each serialized payload once and sends the exact file bytes over localhost TCP. It performs direct unwrapped-object rejection, wrapped inner deserialization twice, wrapped CC6 command execution twice, the same tier-2 bytes against two patched receivers, and the same bytes against `-Djdk.serialFilter=!java.rmi.MarshalledObject`. 5. It validates all assertions and writes `bundle/repro/runtime_manifest.json`. Exit 0 means the claim is confirmed. ## Evidence - **Exact identity:** `bundle/logs/target_identity.txt` binds samples commit `672a1555c7f5670e7affcc7b9984a90b492eb322`, Log4j commit `dd0f9d255e24e6bcc13bd2641407a409c0524803`, official API/Core jar hashes, target digest `7310f18dd2601851bcc37b1963906cbd86f417a18b196251c8fe64a6b3b18673`, and runtime digest `98b16715ef49ca21a5b32b7184a718cb9110ddc5269b06be0c3e3167467df639`. - **Exact attacker input:** `bundle/logs/payload_identity.txt` records the SHA-256 of each pre-generated input. The final tier-2 input has SHA-256 `4e3a504e6d390c37a57253a323d3d91382f8ef92caa95a9e2c2193f38a707217`; sender logs prove identical byte count/file reuse across vulnerable, patched, and mitigated attempts. - **Real TCP/product boundary:** `bundle/logs/receiver_tier2_a1.log` and `receiver_tier2_a2.log` identify loaded `TcpSocketServer`, `ObjectInputStreamLogEventBridge`, vulnerable API/Core jars, and Commons Collections; they then show binding, connection acceptance, socket details, and the deserialized event. - **Command execution:** `bundle/logs/tier2_rce_marker_a1.txt` contains token `RCE-1787737383615632389-15376`, `shell_pid=39677`, and `parent_pid=39626`; attempt a2 contains the same payload token but distinct `shell_pid=39759` and `parent_pid=39708`. Both include `uid=1000(vscode)`, proving execution in two fresh receiver contexts. - **Direct control:** `bundle/logs/receiver_control.log` shows `ObjectInputStreamLogEventBridge.logEvents()` rejecting unwrapped `poc.GadgetOnly` through `FilteredObjectInputStream.resolveClass()`. - **Fixed controls:** `bundle/logs/receiver_fixed_a1.log` and `receiver_fixed_a2.log` show TCP acceptance followed by `InvalidObjectException: Class is not allowed for deserialization: java.rmi.MarshalledObject`; the shared command target is absent. - **Mitigation:** `bundle/logs/receiver_mitigation.log` shows the same TCP/readObject path failing with `InvalidClassException: filter status: REJECTED` and no marker. - **Digest closure:** `bundle/repro/runtime_manifest.json` binds 29 finalized proof artifacts to SHA-256 digests and records `entrypoint_kind=tcp_peer`, service/health/path success, and full target/runtime identity. ## Recommendations / Next Steps Remove `java.rmi.MarshalledObject` from the default Log4j deserialization allowlist and avoid unfiltered `MarshalledObject.get()` for event messages; use a filtered wrapped-object format instead. Upgrade to the first vendor release containing the final upstream correction once published. Do not expose Java serialized-event listeners to untrusted networks. As defense in depth, configure a JEP 290 class filter rejecting `java.rmi.MarshalledObject`, remove unnecessary gadget libraries, and add regression tests that replay a malicious event through the real TCP receiver and require fail-closed rejection. ## Additional Notes The final script was executed successfully twice consecutively after all amendments. Every execution uses isolated receiver JVMs and randomized ports, removes prior proof artifacts, generates a fresh payload token, and verifies exact payload-byte reuse across vulnerable and controls. Service startup, TCP health, and target-path reachability are tracked independently, and a failed run preserves digest-bound evidence from completed phases. The launcher does not implement a socket, parser, or `readObject()` path; it only selects serialized mode through `TcpSocketServer.createSerializedSocketServer()`. ## Reproduction Details Reproduced: 2026-08-26T19:58:13.909Z Duration: 4411 seconds Tool calls: 447 Turns: Unknown Handoffs: 4 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00338 pruva-verify GHSA-LOG4J2-4255-MARSHALLEDOBJECT Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00338&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00338/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-LOG4J2-4255-MARSHALLEDOBJECT - Source: https://github.com/apache/logging-log4j2/discussions/4168 ## Artifacts - bundle/logs/fix.patch (patch, 513 bytes) - bundle/repro/rca_report.md (analysis, 7530 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 27580 bytes) - bundle/logs/capability_observation_a1.json (other, 474 bytes) - bundle/logs/capability_observation_a2.json (other, 474 bytes) - bundle/logs/negative_control_observation.json (other, 488 bytes) - bundle/logs/payload_identity.txt (other, 260 bytes) - bundle/logs/receiver_control.log (log, 2698 bytes) - bundle/logs/receiver_fixed_a1.log (log, 3065 bytes) - bundle/logs/receiver_fixed_a2.log (log, 3065 bytes) - bundle/logs/receiver_mitigation.log (log, 2978 bytes) - bundle/logs/receiver_tier1_a1.log (log, 1667 bytes) - bundle/logs/receiver_tier2_a1.log (log, 1591 bytes) - bundle/logs/receiver_tier2_a2.log (log, 1591 bytes) - bundle/logs/reproduction_steps.log (log, 2656 bytes) - bundle/logs/target_identity.txt (other, 1006 bytes) - bundle/logs/tier1_marker_a1.txt (other, 25 bytes) - bundle/logs/tier2_rce_marker_a1.txt (other, 135 bytes) - bundle/logs/tier2_rce_marker_a2.txt (other, 135 bytes) - bundle/repro/exec_helper.sh (other, 439 bytes) - bundle/repro/exploit_knowledge.json (other, 31860 bytes) - bundle/repro/log4j2-receiver.xml (other, 591 bytes) - bundle/repro/runtime_manifest.json (other, 4371 bytes) - bundle/repro/src/poc/GadgetOnly.java (other, 1096 bytes) - bundle/repro/src/poc/MessageShell.java (other, 739 bytes) - bundle/repro/src/poc/SampleReceiverMain.java (other, 2497 bytes) - bundle/repro/src/poc/Sender.java (other, 4694 bytes) - bundle/repro/validation_verdict.json (other, 1155 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00338 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00338/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00338 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev