
Laboratorio Docker de extremo a extremo que reproduce Apache log4j2 #4255 — omisión de la lista blanca de FilteredObjectInputStream mediante java.rmi.MarshalledObject (deserialización sin filtrar → RCE) en Log4j 2.26.1 / JDK 17.
FilteredObjectInputStream mediante java.rmi.MarshalledObjectUn laboratorio autocontenido y contenerizado que reproduce de extremo a extremo el problema #4255 de Apache log4j2 contra los artefactos oficiales de Log4j 2.26.1 en JDK 17, con controles positivos, un oráculo endurecido y una mitigación validada.
⚠️ Uso responsable
Este laboratorio contiene una RCE de deserialización funcional contra un problema de Log4j actualmente sin parchear (#4255 está ABIERTO /
waiting-for-maintaineral momento de escribir esto; no se ha asignado ningún CVE). Se ejecuta enteramente dentro de contenedores Docker desechables en tu propia máquina y no se conecta a nada externo excepto a Maven Central (para descargar los jars oficiales) — no se contacta ningún objetivo.
- El reportante original (U-Sec / Wujie Security) está reteniendo su PoC a la espera de un parche. Esta es una reproducción independiente construida para la validación de defensores y la ingeniería de detección.
El FilteredObjectInputStream (FOIS) de Log4j es una lista blanca de deserialización basada en
resolveClass. Su lista blanca incluye java.rmi.MarshalledObject. Un MarshalledObject almacena
su carga útil como un byte[] opaco, y MarshalledObject.get() deserializa esa carga útil en
un ObjectInputStream nuevo y sin filtrar — por lo que la lista blanca nunca inspecciona el
grafo interno.
Log4j lo activa por sí mismo: Log4jLogEvent$LogEventProxy (la forma serializada en el cable de un
LogEvent, desde 2.8) transporta el Message del evento dentro de un MarshalledObject y llama a
.get() automáticamente durante la deserialización (readResolve() → message()). Cualquier
aplicación que lea un LogEvent serializado a través de FOIS realiza, por tanto, una
deserialización sin filtrar de bytes del atacante — y como message() se traga la excepción
resultante y recurre a SimpleMessage, el receptor registra un evento benigno y sigue ejecutándose.
El exploit es silencioso.
rel/2.26.1)| # | Ubicación | Defecto |
|---|---|---|
| 1 | log4j-api …/util/internal/SerializationUtil.java | REQUIRED_JAVA_CLASSES contiene java.rmi.MarshalledObject |
| 2 | log4j-api …/util/FilteredObjectInputStream.java | solo sobrescribe resolveClass(); la carga útil objBytes del MarshalledObject es invisible para él |
| 3 | log4j-core …/impl/Log4jLogEvent.java | LogEventProxy.marshalledMessage es un MarshalledObject<Message> |
| 4 | log4j-core …/impl/Log4jLogEvent.java | message() llama a marshalledMessage.get() (sin filtro) y se traga todas las excepciones |
Un sustituto fiel del obsoleto ObjectInputStreamLogEventBridge de log4j-samples: un receptor
TCP no autenticado que lee un LogEvent serializado por conexión a través de FOIS. El atacante
envía un único objeto serializado; el oráculo es un archivo de prueba escrito en un directorio
montado por bind únicamente en el contenedor del receptor, de modo que su aparición demuestra
que el código se ejecutó dentro del receptor mediante deserialización.
| # | Escenario | Classpath de la víctima | jdk.serialFilter | Resultado esperado |
|---|---|---|---|---|
| S1 | gadget no permitido enviado de nivel superior | + gadget | ninguno | rechazo — FOIS aplica su lista blanca |
| S2 | el mismo gadget envuelto en un LogEvent | + gadget | ninguno | rce — auto-activación (necesita la clase del gadget en la víctima) |
| S3 | CommonsCollections6 crudo de nivel superior | log4j + cc-3.2.1 | ninguno | rechazo — FOIS bloquea CC |
| S4 | CC6 insertado en el MarshalledObject | log4j + cc-3.2.1 solo | ninguno | rce — sin clase del atacante en la víctima |
| S5 | carga útil de S4 | log4j + cc-3.2.1 | !java.rmi.MarshalledObject | rechazo — mitigación |
| S6 | carga útil de S4 | log4j + cc-3.2.1 | maxdepth=5;maxbytes=1000000 | silencioso — el flujo interno hereda el filtro; CC6 es demasiado profundo |
| S7 | carga útil de S4 | log4j + cc-3.2.2 | ninguno | silencioso — 3.2.2 deshabilita la deserialización insegura de funtores |
rce = código ejecutado. rechazo = FOIS lanzó una excepción en el flujo externo. silencioso =
objeto externo procesado, no se ejecutó código (bloqueado más adentro, o la versión del gadget es
segura).
./run.sh # o: make run
Requisitos: solo Docker (se descarga un JDK como eclipse-temurin:17-jdk). El script descarga los
jars oficiales y los verifica contra el SHA-1 de Maven Central antes de usarlos. Fija una
versión diferente de Log4j dentro del rango vulnerable con LOG4J_VERSION=2.20.0 ./run.sh.
LogEvent serializado a un
receptor basado en FOIS. No se puede activar haciendo que se registre una cadena (a diferencia
de Log4Shell) — necesita bytes serializados crudos que lleguen al puente de socket.LogEvent → writeReplace()/writeObject() de Log4j envuelve el
Message en un MarshalledObject → el gadget se oculta dentro de su byte[] opaco → en el
receptor, readResolve() → message() → MarshalledObject.get() abre un flujo nuevo sin
filtrar → gadget → Runtime.exec. src/attacker/Attacker.java (poc2) inserta un grafo de
CommonsCollections6 puro en el objBytes del MarshalledObject, por lo que no se necesita
ninguna clase del atacante en la víctima.-Djdk.serialFilter='!java.rmi.MarshalledObject' en la JVM del receptor (S5).
Advertencia: esto también bloquea los objetos LogEventProxy serializados legítimos
(también usan MarshalledObject) — es eficaz pero no transparente para el transporte de logs
serializados.maxdepth/maxbytes. El filtro de todo el proceso
se propaga al flujo interno del MarshalledObject y la profundidad se reinicia allí, por lo
que maxdepth=5 bloquea CC6 (S6) pero un gadget poco profundo pasaría. Depende de la profundidad
de la cadena, no es un límite.MarshalledObject de la lista blanca y mover el mensaje marshalled a los métodos filtrados
writeWrappedObject/readWrappedObject de Log4j.LogEvent serializado no autenticado basado en FOIS y que tenga una versión de
gadget utilizable en su classpath. Las implementaciones ordinarias de Log4j no ejecutan tal
receptor.net.server.TcpSocketServer se sacó de log4j-core en 2017; ausente desde 2.9.0). Los
receptores modernos son código de aplicación/ejemplo, que es lo que este laboratorio modela.uid=0 en el laboratorio es root del contenedor — no hay escape de Docker; la RCE se ejecuta
como el proceso receptor.MarshalledObject burlando un filtro de resolveClass) es arte previo
conocido; ver la discusión de Apache
#4168
("Endurecimiento de deserialización de Log4j 2.x"). La auto-activación específica de Log4j es la
contribución de #4255.run.sh ejecutor portátil (con suma de verificación fijada, oráculo endurecido)
Makefile make build | run | clean
src/victim/Receiver.java receptor de logs FOIS (sustituto de ObjectInputStreamLogEventBridge)
src/attacker/Attacker.java constructor de cargas útiles: controles, PoC-1, PoC-2 (CC6 + inserción de bytes)
src/attacker/EvilMessage.java gadget autocontenido de PoC-1
docs/RESULTS.md matriz de evidencia + análisis
Vulnerabilidad reportada por U-Sec (Wujie Security) en Apache log4j2 #4255. Este repositorio es un laboratorio independiente de reproducción/validación para investigación defensiva e ingeniería de detección.