
End-to-End-Docker-Lab, das Apache log4j2 #4255 reproduziert — FilteredObjectInputStream-Allowlist-Bypass über java.rmi.MarshalledObject (ungefilterte Deserialisierung → RCE) auf Log4j 2.26.1 / JDK 17.
FilteredObjectInputStream-Allowlist-Bypass über java.rmi.MarshalledObjectEin in sich geschlossenes, containerisiertes Labor, das Apache log4j2 Issue #4255 Ende-zu-Ende gegen die offiziellen Log4j-2.26.1-Artefakte auf JDK 17 reproduziert – mit Positivkontrollen, einem gehärteten Orakel und einer validierten Gegenmaßnahme.
⚠️ Verantwortungsvolle Nutzung
Dieses Labor enthält eine funktionierende Deserialisierungs-RCE gegen ein derzeit nicht gepatchtes Log4j-Problem (#4255 ist OPEN /
waiting-for-maintainerzum Zeitpunkt der Erstellung; keine CVE zugewiesen). Es läuft vollständig in wegwerfbaren Docker-Containern auf Ihrem eigenen Rechner und verbindet sich mit nichts Externem außer Maven Central (zum Herunterladen der offiziellen JARs) – es wird kein Ziel kontaktiert.
- Der ursprüngliche Melder (U-Sec / Wujie Security) hält seinen PoC bis zu einem Fix zurück. Dies ist eine unabhängige Reproduktion, die für die Validierung durch Verteidiger und Erkennungsentwicklung erstellt wurde.
- Führen Sie dies nicht gegen Systeme aus, die Sie nicht besitzen oder für die Sie nicht ausdrücklich autorisiert sind.
- Dies demonstriert eine anwendungsbedingte RCE, keine universelle Log4j-RCE (siehe Umfang).
Der FilteredObjectInputStream (FOIS) von Log4j ist eine auf resolveClass basierende
Deserialisierungs-Allowlist. Seine Allowlist enthält java.rmi.MarshalledObject. Ein
MarshalledObject speichert seine Nutzlast als undurchsichtiges byte[], und
MarshalledObject.get() deserialisiert diese Nutzlast auf einem frischen, ungefilterten
ObjectInputStream – die Allowlist inspiziert den inneren Graphen also nie.
Log4j löst dies selbst aus: Log4jLogEvent$LogEventProxy (die serialisierte Drahtform eines
LogEvent, seit 2.8) trägt die Ereignis-Message in einem MarshalledObject und ruft .get()
automatisch während der Deserialisierung auf (readResolve() → message()). Jede Anwendung, die
ein serialisiertes LogEvent über FOIS liest, führt daher eine ungefilterte Deserialisierung
von Angreifer-Bytes durch – und da message() die resultierende Ausnahme verschluckt und auf
SimpleMessage zurückfällt, protokolliert der Empfänger ein harmloses Ereignis und läuft weiter.
Der Exploit ist still.
rel/2.26.1)| # | Ort | Defekt |
|---|---|---|
| 1 | log4j-api …/util/internal/SerializationUtil.java | REQUIRED_JAVA_CLASSES enthält java.rmi.MarshalledObject |
| 2 | log4j-api …/util/FilteredObjectInputStream.java | überschreibt nur resolveClass(); die MarshalledObject-objBytes-Nutzlast ist für ihn unsichtbar |
| 3 | log4j-core …/impl/Log4jLogEvent.java | LogEventProxy.marshalledMessage ist ein MarshalledObject<Message> |
| 4 | log4j-core …/impl/Log4jLogEvent.java | message() ruft marshalledMessage.get() (filterlos) auf und verschluckt alle Ausnahmen |
Ein getreuer Stellvertreter für die veraltete log4j-samples-ObjectInputStreamLogEventBridge: ein
nicht authentifizierter TCP-Empfänger, der ein serialisiertes LogEvent pro Verbindung über FOIS
liest. Der Angreifer sendet ein einzelnes serialisiertes Objekt; das Orakel ist eine
Beweisdatei, die in ein Verzeichnis geschrieben wird, das nur in den Empfänger-Container
bind-gemountet ist, sodass ihr Erscheinen beweist, dass Code im Empfänger über Deserialisierung
ausgeführt wurde.
| # | Szenario | Victim-Classpath | jdk.serialFilter | Erwartet |
|---|---|---|---|---|
| S1 | nicht erlaubtes Gadget top-level gesendet | + Gadget | keine | ablehnen — FOIS erzwingt seine Allowlist |
| S2 | dasselbe Gadget eingewickelt in ein LogEvent | + Gadget | keine | rce — automatischer Auslöser (benötigt die Gadget-Klasse auf dem Opfer) |
| S3 | rohes CommonsCollections6 top-level | log4j + cc-3.2.1 | keine | ablehnen — FOIS blockiert CC |
| S4 | CC6 in das MarshalledObject eingefügt | log4j + cc-3.2.1 nur | keine | rce — keine Angreifer-Klasse auf dem Opfer |
| S5 | S4-Nutzlast | log4j + cc-3.2.1 | !java.rmi.MarshalledObject | ablehnen — Gegenmaßnahme |
| S6 | S4-Nutzlast | log4j + cc-3.2.1 | maxdepth=5;maxbytes=1000000 | still — innerer Stream erbt den Filter; CC6 zu tief |
| S7 | S4-Nutzlast | log4j + cc-3.2.2 | keine | still — 3.2.2 deaktiviert unsichere Functor-Deserialisierung |
rce = Code ausgeführt. reject = FOIS warf auf dem äußeren Stream. silent = äußeres Objekt
verarbeitet, kein Code ausgeführt (tiefer blockiert, oder Gadget-Version ist sicher).
./run.sh # oder: make run
Anforderungen: nur Docker (ein JDK wird als eclipse-temurin:17-jdk gezogen). Das Skript lädt die
offiziellen JARs herunter und verifiziert sie vor der Verwendung gegen die SHA-1 von Maven
Central. Eine andere Log4j-Version im verwundbaren Bereich festlegen mit
LOG4J_VERSION=2.20.0 ./run.sh.
LogEvent an einen FOIS-basierten Empfänger. Es ist nicht durch das Protokollieren eines
Strings auslösbar (anders als Log4Shell) – es benötigt rohe serialisierte Bytes, die die
Socket-Bridge erreichen.LogEvent erstellen → writeReplace()/writeObject() von Log4j wickelt
die Message in ein MarshalledObject → das Gadget versteckt sich in seinem undurchsichtigen
byte[] → auf dem Empfänger: readResolve() → message() → MarshalledObject.get() öffnet
einen frischen ungefilterten Stream → Gadget → Runtime.exec. src/attacker/Attacker.java
(poc2) fügt einen reinen CommonsCollections6-Graphen in die objBytes des
MarshalledObject ein, sodass keine Angreifer-Klasse auf dem Opfer benötigt wird.-Djdk.serialFilter='!java.rmi.MarshalledObject' auf der Empfänger-JVM (S5).
Einschränkung: dies blockiert auch legitime serialisierte LogEventProxy-Objekte (sie
verwenden ebenfalls MarshalledObject) – es ist wirksam, aber nicht transparent für
serialisierten Log-Transport.maxdepth/maxbytes-Filter. Der prozessweite Filter pflanzt
sich in den inneren MarshalledObject-Stream fort und die Tiefe startet dort neu, sodass
maxdepth=5 CC6 blockiert (S6), aber ein flaches Gadget durchkommen würde.
Ketten-Tiefen-abhängig, keine Grenze.MarshalledObject aus der Allowlist entfernen und die marshalled Message auf Log4js
gefilterte writeWrappedObject/readWrappedObject umstellen.