{"repro_id":"REPRO-2026-00338","version":6,"title":"Apache Log4j2 serialized LogEvent filter bypass to conditional RCE","repro_type":"security","status":"published","severity":"critical","description":"Demonstrate **remote, unauthenticated arbitrary deserialization -> RCE** against Apache Log4j2's `FilteredObjectInputStream` (FOIS) via a `java.rmi.MarshalledObject` allowlist bypass, auto-triggered during `LogEvent` deserialization. Target: the FOIS-based serialized-socket log receiver.","root_cause":"# Root Cause Analysis\n\n## Summary\nApache 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.\n\n## Impact\n- **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.\n- **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.\n- **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.\n\n## Impact Parity\n- **Disclosed/claimed maximum impact:** Remote unauthenticated arbitrary deserialization leading to code execution.\n- **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.\n- **Parity:** `full`.\n- **Not demonstrated:** Privilege escalation beyond the account running the receiver was not claimed or tested.\n\n## Root Cause\nVulnerable `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`.\n\nThe 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.\n\n## Reproduction Steps\n1. Run `bash bundle/repro/reproduction_steps.sh` from any directory. The script uses `PRUVA_ROOT` or resolves the bundle path itself.\n2. It reads `bundle/project_cache_context.json`, uses `<project_cache_dir>/repo` when prepared, verifies the Apache samples origin and exact commit, and extracts the real `log4j-server` sources.\n3. 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.\n4. 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`.\n5. It validates all assertions and writes `bundle/repro/runtime_manifest.json`. Exit 0 means the claim is confirmed.\n\n## Evidence\n- **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`.\n- **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.\n- **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.\n- **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.\n- **Direct control:** `bundle/logs/receiver_control.log` shows `ObjectInputStreamLogEventBridge.logEvents()` rejecting unwrapped `poc.GadgetOnly` through `FilteredObjectInputStream.resolveClass()`.\n- **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.\n- **Mitigation:** `bundle/logs/receiver_mitigation.log` shows the same TCP/readObject path failing with `InvalidClassException: filter status: REJECTED` and no marker.\n- **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.\n\n## Recommendations / Next Steps\nRemove `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.\n\n## Additional Notes\nThe 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()`.\n","ghsa_id":"GHSA-LOG4J2-4255-MARSHALLEDOBJECT","cwe_id":"CWE-502","source_url":"https://github.com/apache/logging-log4j2/discussions/4168","package":{"name":"org.apache.logging.log4j:log4j-core","ecosystem":"maven","affected_versions":"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_version":"none yet - upstream issue open, waiting-for-maintainer label; no CVE/GHSA assigned","tested_vulnerable":"2.26.1"},"reproduced_at":"2026-08-26T19:58:13.909720+00:00","duration_secs":4411.0,"tool_calls":447,"handoffs":4,"total_cost_usd":14.109183,"agent_costs":{"judge":2.954824,"learning_policy":0.017199,"repro":11.073075,"support":0.064085},"cost_breakdown":{"judge":{"gpt-5.6-sol":2.954824},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.017199},"repro":{"accounts/fireworks/models/kimi-k3":4.305915,"gpt-5.6-sol":6.76716},"support":{"accounts/fireworks/models/kimi-k3":0.064085}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-08-26T19:58:14.789291+00:00","retracted":false,"artifacts":[{"path":"bundle/logs/fix.patch","filename":"fix.patch","size":513,"category":"patch"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":7530,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":27580,"category":"reproduction_script"},{"path":"bundle/logs/capability_observation_a1.json","filename":"capability_observation_a1.json","size":474,"category":"other"},{"path":"bundle/logs/capability_observation_a2.json","filename":"capability_observation_a2.json","size":474,"category":"other"},{"path":"bundle/logs/negative_control_observation.json","filename":"negative_control_observation.json","size":488,"category":"other"},{"path":"bundle/logs/payload_identity.txt","filename":"payload_identity.txt","size":260,"category":"other"},{"path":"bundle/logs/receiver_control.log","filename":"receiver_control.log","size":2698,"category":"log"},{"path":"bundle/logs/receiver_fixed_a1.log","filename":"receiver_fixed_a1.log","size":3065,"category":"log"},{"path":"bundle/logs/receiver_fixed_a2.log","filename":"receiver_fixed_a2.log","size":3065,"category":"log"},{"path":"bundle/logs/receiver_mitigation.log","filename":"receiver_mitigation.log","size":2978,"category":"log"},{"path":"bundle/logs/receiver_tier1_a1.log","filename":"receiver_tier1_a1.log","size":1667,"category":"log"},{"path":"bundle/logs/receiver_tier2_a1.log","filename":"receiver_tier2_a1.log","size":1591,"category":"log"},{"path":"bundle/logs/receiver_tier2_a2.log","filename":"receiver_tier2_a2.log","size":1591,"category":"log"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":2656,"category":"log"},{"path":"bundle/logs/target_identity.txt","filename":"target_identity.txt","size":1006,"category":"other"},{"path":"bundle/logs/tier1_marker_a1.txt","filename":"tier1_marker_a1.txt","size":25,"category":"other"},{"path":"bundle/logs/tier2_rce_marker_a1.txt","filename":"tier2_rce_marker_a1.txt","size":135,"category":"other"},{"path":"bundle/logs/tier2_rce_marker_a2.txt","filename":"tier2_rce_marker_a2.txt","size":135,"category":"other"},{"path":"bundle/repro/exec_helper.sh","filename":"exec_helper.sh","size":439,"category":"other"},{"path":"bundle/repro/exploit_knowledge.json","filename":"exploit_knowledge.json","size":31860,"category":"other"},{"path":"bundle/repro/log4j2-receiver.xml","filename":"log4j2-receiver.xml","size":591,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":4371,"category":"other"},{"path":"bundle/repro/src/poc/GadgetOnly.java","filename":"GadgetOnly.java","size":1096,"category":"other"},{"path":"bundle/repro/src/poc/MessageShell.java","filename":"MessageShell.java","size":739,"category":"other"},{"path":"bundle/repro/src/poc/SampleReceiverMain.java","filename":"SampleReceiverMain.java","size":2497,"category":"other"},{"path":"bundle/repro/src/poc/Sender.java","filename":"Sender.java","size":4694,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1155,"category":"other"}]}