
Laboratoire Docker de bout en bout reproduisant Apache log4j2 #4255 — contournement de la liste blanche de FilteredObjectInputStream via java.rmi.MarshalledObject (désérialisation non filtrée → RCE) sur Log4j 2.26.1 / JDK 17.
FilteredObjectInputStream via java.rmi.MarshalledObjectUn laboratoire autonome et conteneurisé qui reproduit de bout en bout le problème Apache log4j2 #4255 contre les artefacts officiels Log4j 2.26.1 sur JDK 17, avec des contrôles positifs, un oracle durci et une atténuation validée.
⚠️ Utilisation responsable
Ce laboratoire contient une RCE par désérialisation fonctionnelle contre un problème Log4j actuellement non corrigé (#4255 est OUVERT /
waiting-for-maintainerau moment de la rédaction ; aucun CVE attribué). Il s'exécute entièrement dans des conteneurs Docker jetables sur votre propre machine et ne se connecte à rien d'externe sauf Maven Central (pour télécharger les jars officiels) — aucune cible n'est contactée.
- Le rapporteur d'origine (U-Sec / Wujie Security) retient son PoC en attendant un correctif. Il s'agit d'une reproduction indépendante conçue pour la validation par les défenseurs et l'ingénierie de détection.
Le FilteredObjectInputStream (FOIS) de Log4j est une liste blanche de désérialisation basée sur resolveClass.
Sa liste blanche inclut java.rmi.MarshalledObject. Un MarshalledObject stocke sa charge utile sous forme de
byte[] opaque, et MarshalledObject.get() désérialise cette charge utile sur un ObjectInputStream frais et non filtré
— la liste blanche n'inspecte donc jamais le graphe interne.
Log4j déclenche cela lui-même : Log4jLogEvent$LogEventProxy (la forme filaire sérialisée d'un LogEvent,
depuis 2.8) transporte le Message de l'événement dans un MarshalledObject et appelle .get() automatiquement
pendant la désérialisation (readResolve() → message()). Toute application qui lit un LogEvent sérialisé
via FOIS effectue donc une désérialisation non filtrée d'octets attaquants — et
comme message() avale l'exception résultante et retombe sur SimpleMessage, le récepteur
journalise un événement bénin et continue de fonctionner. L'exploit est silencieux.
rel/2.26.1)| # | Emplacement | Défaut |
|---|---|---|
| 1 | log4j-api …/util/internal/SerializationUtil.java | REQUIRED_JAVA_CLASSES contient java.rmi.MarshalledObject |
| 2 | log4j-api …/util/FilteredObjectInputStream.java | ne surcharge que resolveClass() ; la charge utile objBytes du MarshalledObject lui est invisible |
| 3 | log4j-core …/impl/Log4jLogEvent.java | LogEventProxy.marshalledMessage est un MarshalledObject<Message> |
| 4 | log4j-core …/impl/Log4jLogEvent.java | message() appelle marshalledMessage.get() (sans filtre) et avale toutes les exceptions |
Un substitut fidèle du ObjectInputStreamLogEventBridge déprécié de log4j-samples : un
récepteur TCP non authentifié qui lit un LogEvent sérialisé par connexion via FOIS.
L'attaquant envoie un seul objet sérialisé ; l'oracle est un fichier de preuve écrit dans un répertoire
monté en bind uniquement dans le conteneur du récepteur, de sorte que son apparition prouve que du code s'est exécuté dans le
récepteur via la désérialisation.
| # | Scénario | Classpath de la victime | jdk.serialFilter | Attendu |
|---|---|---|---|---|
| S1 | gadget interdit envoyé au niveau supérieur | + gadget | aucun | rejet — FOIS applique sa liste blanche |
| S2 | même gadget enveloppé dans un LogEvent | + gadget | aucun | rce — déclenchement automatique (nécessite la classe du gadget sur la victime) |
| S3 | CommonsCollections6 brut au niveau supérieur | log4j + cc-3.2.1 | aucun | rejet — FOIS bloque CC |
| S4 | CC6 inséré dans le MarshalledObject | log4j + cc-3.2.1 uniquement | aucun | rce — aucune classe attaquante sur la victime |
| S5 | charge utile S4 | log4j + cc-3.2.1 | !java.rmi.MarshalledObject | rejet — atténuation |
| S6 | charge utile S4 | log4j + cc-3.2.1 | maxdepth=5;maxbytes=1000000 | silencieux — le flux interne hérite du filtre ; CC6 trop profond |
| S7 | charge utile S4 | log4j + cc-3.2.2 | aucun | silencieux — 3.2.2 désactive la désérialisation de foncteurs non sûrs |
rce = code exécuté. reject = FOIS a levé une exception sur le flux externe. silent = objet externe traité,
aucun code exécuté (bloqué plus profondément, ou version du gadget sûre).
./run.sh # ou : make run
Prérequis : Docker uniquement (un JDK est tiré comme eclipse-temurin:17-jdk). Le script télécharge les
jars officiels et les vérifie contre les SHA-1 de Maven Central avant utilisation. Épinglez une version Log4j
différente dans la plage vulnérable avec LOG4J_VERSION=2.20.0 ./run.sh.
LogEvent sérialisé vers un
récepteur basé sur FOIS. Ce n'est pas déclenchable en faisant journaliser une chaîne (contrairement à Log4Shell) — il
faut des octets sérialisés bruts atteignant le pont socket.LogEvent → writeReplace()/writeObject() de Log4j enveloppe le Message
dans un MarshalledObject → le gadget se cache dans son byte[] opaque → sur le récepteur,
readResolve() → message() → MarshalledObject.get() ouvre un flux non filtré frais →
gadget → Runtime.exec. src/attacker/Attacker.java (poc2) insère un graphe pure-CommonsCollections6
dans objBytes du MarshalledObject, donc aucune classe attaquante n'est nécessaire sur la victime.-Djdk.serialFilter='!java.rmi.MarshalledObject' sur la JVM du récepteur (S5).
Mise en garde : cela bloque aussi les objets LogEventProxy sérialisés légitimes (ils utilisent
MarshalledObject également) — c'est efficace mais pas transparent pour le transport de journaux sérialisés.maxdepth/maxbytes. Le filtre à l'échelle du processus se propage dans
le flux MarshalledObject interne et la profondeur y redémarre, donc maxdepth=5 bloque CC6 (S6)
mais un gadget peu profond passerait. Dépendant de la profondeur de chaîne, pas une frontière.MarshalledObject de la liste blanche et déplacer
le message marshallé vers writeWrappedObject/readWrappedObject filtrés de Log4j.LogEvent sérialisé non authentifié basé sur FOIS et qui a une version de gadget utilisable sur
son classpath. Les déploiements Log4j ordinaires n'exécutent aucun récepteur de ce type.net.server.TcpSocketServer
a été déplacé hors de log4j-core en 2017 ; absent à partir de 2.9.0). Les récepteurs modernes sont du
code d'application/échantillon, ce que ce laboratoire modélise.uid=0 dans le laboratoire est root de conteneur — il n'y a pas d'évasion Docker ; la RCE s'exécute comme le
processus du récepteur.MarshalledObject contournant un filtre resolveClass) est un art antérieur connu ;
voir la discussion Apache #4168
(« Log4j 2.x deserialization hardening »). Le déclenchement automatique spécifique à Log4j est la contribution de #4255.run.sh exécuteur portable (épinglé par somme de contrôle, oracle durci)
Makefile make build | run | clean
src/victim/Receiver.java récepteur de journaux FOIS (substitut ObjectInputStreamLogEventBridge)
src/attacker/Attacker.java constructeur de charges utiles : contrôles, PoC-1, PoC-2 (CC6 + insertion d'octets)
src/attacker/EvilMessage.java gadget autonome PoC-1
docs/RESULTS.md matrice de preuves + analyse
Vulnérabilité signalée par U-Sec (Wujie Security) dans Apache log4j2 #4255. Ce dépôt est un laboratoire indépendant de reproduction/validation pour la recherche défensive et l'ingénierie de détection.