Skip to main content

Krampus trae una vulnerabilidad de Struts para cerrar el año

Escrito por
feature snyk honeycomb

2 de enero de 2024

0 minutos de lectura

Nota del editor: 16 de enero de 2024

Se actualizó un ejemplo de código en este blog para reforzar la seguridad de la solución.

El 20 de diciembre de 2023, NIST actualizó un CVE para reflejar una nueva vulnerabilidad de recorrido de rutas en struts-core. Se trata de CVE-2023-50164, también incluida en la Snyk Vulnerability Database, con una gravedad crítica de 9.8 en CVSS. Si llevas suficiente tiempo trabajando en ciberseguridad, recordarás la filtración de datos de Equifax en 2017, que también se produjo debido a una vulnerabilidad de Struts sin parche.

En esta publicación, explico el problema, analizo su gravedad, te guío por una explotación de prueba de concepto y te doy recomendaciones para remediarlo. Además, si usas Snyk (regístrate gratis aquí), puedes recibir notificaciones fácilmente sobre esta y otras vulnerabilidades, y obtener consejos para corregir el código problemático.

Empecemos con las recomendaciones para remediarla. Solo tienes que actualizar tu versión de Struts a la 2.5.33 o la 6.3.0.2 (o posterior), según la versión base que uses actualmente.

¿Qué tan grave es esta vulnerabilidad de recorrido de rutas CVE-2023-50164 de Struts?

Hemos visto el ejemplo más extremo de una vulnerabilidad en Struts que puede conducir a la ejecución de código arbitrario. Esa vulnerabilidad provocó en 2017 la conocida filtración de datos de Equifax. La explotación se logró enviando un encabezado Content-Type malformado y aprovechando la excepción resultante. Era el peor tipo de vulnerabilidad, porque podía explotarse de forma remota y en ese momento no había ninguna protección en el código.

Esta nueva vulnerabilidad es grave: asegúrate de actualizar tu versión de Struts. Aun así, es menos grave que la vulnerabilidad de 2017, ya que requiere tanto código inseguro como una versión vulnerable de Struts.

Esta vulnerabilidad permite el recorrido de rutas al subir un archivo. Es decir, puedes subir un archivo y «salir» de la carpeta de carga designada al proporcionar una ruta relativa. Esto puede conducir a la ejecución remota de código, ya que la ruta relativa puede apuntar a carpetas servidas por la aplicación.

Explotación de prueba de concepto de recorrido de rutas para CVE-2023-50164 de Struts

Todo el código de esta publicación está en GitHub.

Este es un proyecto de Java que usa Maven para las compilaciones. El proyecto tiene dos perfiles: uno se llama vuln (es el predeterminado) y el otro, no-vuln. Si no conoces Maven o sus perfiles, ¡no te preocupes! Es muy fácil ejecutarlo y el perfil vulnerable es el predeterminado. El único requisito es tener Java Runtime versión 17 o posterior. 

Ve a la carpeta del proyecto que clonaste y ejecuta lo siguiente:

./mvnw clean jetty:run

Puedes acceder al servidor en http://localhost:9999/struts-vuln-poc. Allí verás una interfaz muy sencilla para subir archivos. Sin embargo, no dedicaremos tiempo a eso. Para demostrar que no hay trucos ocultos, ve a http://localhost:9999/struts-vuln-poc/rogue.jsp. Deberías ver una página 404 como esta:

Página de error HTTP 404 que indica que no se encontró el archivo JSP /rogue.jsp, con tecnología de Jetty 9.4.46

Queremos explotar esta vulnerabilidad, así que abre una terminal, ve a la carpeta del proyecto y prepárate para usar el cliente HTTP que prefieras. En mi ejemplo uso curl, pero puedes usar cualquier cliente HTTP que admita la carga de archivos. Ejecuta el siguiente comando:

curl \
http://localhost:9999/struts-vuln-poc/upload.action \
-F "Upload=@./payload/rogue.jsp" \
-F "uploadFileName=../src/main/webapp/rogue.jsp"

Ahora, ve a http://localhost:9999/struts-vuln-poc/rogue.jsp. Deberías ver el mensaje: Ya been PWNED!.

Ahora que explotamos la vulnerabilidad al colocar un archivo malicioso en una carpeta servida por nuestra aplicación, veamos cómo llegamos a esto.

Como mencioné antes, para que esta explotación funcione, se necesita tanto una versión vulnerable de la biblioteca Struts como código inseguro.

Primero, veamos la parte del código inseguro. La acción Upload tiene tres propiedades:

private File upload;
private String uploadFileName;
private String uploadContentType;

Por la forma en que funciona Struts, estas propiedades se completan con la solicitud enviada desde un cliente HTTP (ya sea tu navegador o una utilidad de línea de comandos, como curl).

El método execute procesa la solicitud después de que se hayan establecido las propiedades.

Este es el núcleo del método execute:

String uploadDirectory = System.getProperty("user.dir") + "/uploads/";
File destFile = new File(uploadDirectory, uploadFileName);
FileUtils.copyFile(upload, destFile);

¿Puedes detectar el problema? En lugar de confiar en nuestra capacidad para detectar errores, podemos usar Snyk para identificar el problema en este código. Snyk cuenta con una interfaz de línea de comandos (CLI), integraciones con IDE y una interfaz web, para que puedas usarlo dondequiera que trabajes. 

Estos son los resultados de ejecutar snyk code test desde la CLI para usar las capacidades de análisis estático de seguridad de aplicaciones (SAST) de Snyk:

Terminal que muestra cómo el análisis estático de Snyk detecta una vulnerabilidad de recorrido de rutas de gravedad alta en Upload.java, línea 23.

Este resultado muestra el número de línea específico donde se encuentra la vulnerabilidad de seguridad, así como su tipo: Path Traversal. También puedo ver la gravedad del problema: High.

Estos son los resultados de ejecutar snyk test desde la CLI para usar las capacidades de análisis de composición de software (SCA) de Snyk:

Salida de terminal de Snyk test que muestra una vulnerabilidad crítica de ejecución remota de código en Apache Struts y recomienda actualizar de la versión 6.3.0.1 a la 6.3.0.2

Este resultado muestra que tengo una dependencia con una vulnerabilidad conocida: mi versión de struts-core, la versión 6.3.0.1. También recomienda actualizarla a la versión 6.3.0.2.

Ambos resultados son muy útiles y permiten tomar medidas. Pero la integración con el IDE ofrece una vista aún más completa y útil.

Tengo el proyecto cargado en IntelliJ IDEA, que es lo que uso para desarrollar en Java. Al ejecutar el análisis de Snyk en el proyecto, obtengo estos resultados:

La extensión de Snyk para IntelliJ IDEA detecta una vulnerabilidad de ejecución remota de código en un componente de Struts.

En el panel izquierdo veo los resultados de SAST y SCA. En el panel derecho, veo las recomendaciones específicas para remediar el problema.

Y aún más útil: cuando hago clic en el problema del código, aparece un panel que muestra la corrección de código de otros tres proyectos de código abierto que tenían el mismo problema:

La extensión de Snyk para IntelliJ IDEA detecta una vulnerabilidad de recorrido de ruta en Struts.

Veamos el ejemplo de google/j2objc. Al desplazarme hacia abajo, veo las líneas de código modificadas para corregir esta vulnerabilidad, presentadas en formato diff:

Análisis de seguridad de Snyk que muestra una vulnerabilidad crítica de Apache Struts y una diferencia de código que agrega una comprobación de validación de ruta canónica

Esto me da una gran ventaja para corregir el código, sobre todo si no conozco bien la vulnerabilidad.

Corrección de la vulnerabilidad de recorrido de rutas

Con la información de las herramientas de análisis de Snyk, ahora puedo corregir tanto mi código personalizado como mis dependencias.

Primero, enfoquémonos en el código. Ejecuta lo siguiente con curl u otro cliente HTTP:

curl \
http://localhost:9999/struts-vuln-poc/upload-no-vuln.action \
-F "Upload=@./payload/rogue.jsp" \
-F "uploadFileName=../src/main/webapp/rogue.jsp"

Ahora verás un mensaje de error en el resultado: Attempted path traversal attack. Con las sugerencias que obtuve del análisis de <código Snyk Code>, actualicé el código así:

File uploadDirectory = new File(USER_DIR + "/uploads/");
File destFile = new File(uploadDirectory, uploadFileName);

if (
  !destFile.getCanonicalPath().startsWith(uploadDirectory.getCanonicalPath() + File.separator)
  !upload.getCanonicalPath().startsWith(USER_DIR)
) {
  throw new SecurityException("Attempted path traversal attack");
}

FileUtils.copyFile(upload, destFile);

La instrucción if comprueba que destFile use la ruta canónica del directorio uploads que definimos. También comprueba que el archivo cargado esté dentro de las carpetas definidas para la aplicación y no provenga de una ubicación maliciosa. Al subir un archivo, Struts lo coloca automáticamente en una carpeta temporal dentro de su classpath. Si estas comprobaciones fallan, se lanza una SecurityException.

Esto mejora mucho el código original y lo protege contra el recorrido de rutas.

Hay un detalle que vale la pena mencionar. Observa que la comprobación incluye una barra diagonal final (/) mediante la llamada File.separator. Esto es muy importante porque, sin ella, un usuario malicioso aún podría salir de la carpeta de cargas mediante una explotación con una ruta parcial. Esto se debe a que getCanonicalPath() siempre devuelve un valor sin la barra diagonal final. Jonathan Leitschuh lo explica muy bien en su charla de DefCon: Jonathan Leitschuh. Una corrección aún más sólida usa getCanonicalFile().toPath(), incluida en el repositorio de esta publicación. 

La idea principal es que actualizar a la versión más reciente de Struts es la mejor y más sólida forma de resolver el problema del recorrido de rutas. No tendrás que depender de comprobaciones manuales en el código, ya que las rutas relativas se eliminan automáticamente.

Podemos mejorar aún más nuestra postura de seguridad si aplicamos los otros cambios que recomienda el análisis de Snyk. Es decir, podemos actualizar struts-core de la versión 6.3.0.1 a la 6.3.0.2.

El proyecto del repositorio que clonaste antes tiene un perfil que usa la versión actualizada de struts-core. Detén la aplicación en ejecución y elimina el archivo src/main/webapp/rogue.jsp que subiste antes. Reiníciala con el siguiente comando:

./mvnw clean jetty:run -P no-vuln

Ahora, ejecuta el comando curl original de antes:

curl \
http://localhost:9999/struts-vuln-poc/upload.action \
-F "Upload=@./payload/rogue.jsp" \
-F "uploadFileName=../src/main/webapp/rogue.jsp"

Esta vez, obtendrás un mensaje de éxito como este: File uploaded successfully to /CVE-2023-50164-POC/uploads/rogue.jsp

Observa que se ignoró la ruta relativa que proporcionamos. Quizá te preguntes por qué no apareció el mensaje de error de antes. Esto se debe a que la versión actualizada de struts-core depura automáticamente las rutas proporcionadas como entrada. 

Si estableces un punto de interrupción en la línea 23 de la clase Upload.java, verás lo siguiente al hacer la solicitud con curl:

Vista del depurador que muestra uploadFileName configurado como "rogue.jsp", junto con las variables del directorio de carga y del archivo de destino.

Se eliminó de uploadFileName la ruta relativa que proporcioné.

Anticípate a las vulnerabilidades con Snyk

Espero que te haya resultado útil esta demostración de la vulnerabilidad de recorrido de rutas en versiones anteriores de struts-core. Todo el código de esta publicación está en GitHub.

Las herramientas de análisis de Snyk detectaron el problema tanto en la definición de dependencias del proyecto como en el código personalizado.

Puedes aumentar aún más tu productividad si usas Snyk para supervisar activamente tu proyecto y recibir notificaciones automáticas cuando se descubran nuevas vulnerabilidades. Snyk incluso puede crear automáticamente un pull request (PR) en tu nombre cuando encuentra una nueva vulnerabilidad. ¡Solo tienes que revisarlo y fusionarlo!

En lugar de revisar CVE y comprobar manualmente si tu código tiene algún problema, puedes enfocarte en lo que mejor haces: ¡escribir código! Prueba Snyk gratis en: https://app.snyk.io

Más recursos para lidiar con Apache Struts:

Empieza con los desafíos de Capture the Flag

Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.