
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.
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.LogEvent-Empfänger bereitstellt und eine
nutzbare Gadget-Version auf ihrem Classpath hat. Gewöhnliche Log4j-Bereitstellungen führen
keinen solchen Empfänger aus.net.server.TcpSocketServer
wurde 2017 aus log4j-core entfernt; ab 2.9.0 nicht vorhanden). Moderne Empfänger sind
Anwendungs-/Beispielcode, den dieses Labor modelliert.uid=0 im Labor ist Container-Root – es gibt keinen Docker-Escape; die RCE läuft als
Empfängerprozess.MarshalledObject, das einen resolveClass-Filter aushebelt) ist bekannter
Stand der Technik; siehe Apache-Diskussion
#4168
(„Log4j 2.x deserialization hardening"). Der Log4j-spezifische automatische Auslöser ist der
Beitrag von #4255.run.sh portabler Runner (prüfsummen-pinned, gehärtetes Orakel)
Makefile make build | run | clean
src/victim/Receiver.java FOIS-Log-Empfänger (ObjectInputStreamLogEventBridge-Stellvertreter)
src/attacker/Attacker.java Nutzlast-Ersteller: Kontrollen, PoC-1, PoC-2 (CC6 + Byte-Splice)
src/attacker/EvilMessage.java PoC-1 in sich geschlossenes Gadget
docs/RESULTS.md Evidenz-Matrix + Analyse
Schwachstelle gemeldet von U-Sec (Wujie Security) in Apache log4j2 #4255. Dieses Repository ist ein unabhängiges Reproduktions-/Validierungslabor für defensive Forschung und Erkennungsentwicklung.