From Spring to Hell: Exploring the Spring4Shell vulnerability

vsociety
12 min readJul 5, 2023

--

Introduction

Spring4Shell (CVE-2022–22965), a significant vulnerability in the Spring Framework, was identified in the latter part of March 2022. The severity of this vulnerability is reflected by its critical CVSS rating of 9.8, which exposes affected systems to the possibility of remote code execution (RCE). In short, an attacker can exploit the system by manipulating the Tomcat logging settings through the exposed classloader and overwriting arbitrary strings into a designated file so creating a webshell can be possible.

To grasp the gravity of Spring4Shell as a vulnerability, it is beneficial to gain an understanding of the underlying workings of the Spring Framework which is a popular Java-based open-source framework that provides comprehensive infrastructure support.

Affected versions and prerequisites

The vulnerability affects SpringMVC and Spring WebFlux applications with the following conditions:

  • JDK 9 or higher (because module property was added in Java 9).
  • Spring Framework versions are 5.3.0 to 5.3.17, 5.2.0 to 5.2.19, and older versions.
  • The application is running on an Apache Tomcat server as a web application archive (WAR) deployment. The exploit we will use works only on Tomcat because of its special classloader. (Although a similar reference chain may exist on other web application servers as well.)
  • The app should use Spring non-basic parameter binding.

It may seem that there are too many conditions that need to align for the vulnerability to be exploitable but in an analysis of Java Technologies made by JetBrains in 2022[1], it has been revealed that Spring Boot and Spring MVC enjoy the highest levels of popularity among Java frameworks. Additionally, the report identifies Apache Tomcat as the leading Java application server in terms of usage.

Lab setup

I was working on Kali Linux but you can use any other Linux distributions as well.

Installing Docker

apt update
apt install docker

Cloning the repo

Then, clone the repo I created containing both a containerized dummy web application vulnerable to Spring4Shell vulnerability and an exploit for it.

git clone https://github.com/jakabakos/spring4shell.git

Build and run the image

As a next step, build the Docker image locally:

cd spring4shell
docker build . -t spring4shell

First, run the image with the following command:

docker run -p 80:8080 –p 5005:5005 spring4shell

You can make sure about the image is running with the command docker ps.

Now you should reach the dummy web app under http://localhost/spring4shell/.

Reproducing the vulnerability

Now the application is running so as a next step we make the exploit executable and run it with the following commands:

chmod +x ./exploit.sh
./exploit.sh --url http://localhost/spring4shell/hello --dir spring4shell

(The –dir arg is optional and if not specified, then the shell.jsp will be uploaded to the Tomcat ROOT directory therefore the webshell will be on the root domain.)

If all goes well you should see something similar to this:

Now you can run any bash command remotely with shell.jsp‘s cmd parameter. Let’s see what we get for a command id:

curl http://localhost/spring4shell/shell.jsp?cmd=id --output –

And… There we are — RCE in da house!

Of course, you can get the same results by pasting the URL in a browser.

Static analysis

The vulnerable code

The vulnerability affects applications with controllers using @RequestMapping annotation and POJOs (Plain Old Java Objects) as parameters. Below is an example controller class (HelloController), which includes a PostMapping for "/hello" endpoint. When navigating http://localhost/spring4shell/hello through a platform like Tomcat, this controller will handle the request and also attempt to convert the input into a DemoObject POJO.

//HelloController.java
@Controller
public class HelloController {
@PostMapping("/hello")
public String welcome(@ModelAttribute DemoObject demo) {
return "index";
}
}
//DemoObject.java
public class DemoObject {
private String message;
public String getMessage() { return message; }
public void setMessage(String content) { this.message = content; }
}

So Spring simplifies developers’ lives by translating the content and parameters of an HTTP request into an object that they can easily work with. During the construction of this object, Spring takes precautions to prevent attackers from manipulating any aspects of the Class and ClassLoader related to the created instance (see patch diffing section for more details). However, due to modifications introduced to the Class object in Java 9, the existing security checks performed by Spring became inadequate.

The heart of the vulnerability

In the context of Spring, the underlying mechanism relies on a special mechanism to map values got from a request to POJOs or Java objects. The DemoObject class above has one message field, but actually it also has a reference to the Class object[2], and since Spring also supports binding of nested fields (e.g. employee.data.name), we can use class.module.classLoader as a form data key to access the classloader. The classloader is responsible for loading Java classes into memory at runtime. A module is a self-contained unit of code that encapsulates its implementation details and exposes a well-defined set of APIs to other modules. Note that the Class object exposes a getModule() method from Java 9 only.

This behavior enables the setting of public properties for classes that are accessible through a nested reference chain starting from the DemoObject class. It provides a convenient way to manipulate and modify the properties of related classes in the hierarchy.

Tomcat joins the game

In many cases, this aforementioned behavior is not inherently risky since there are no classes with public fields accessible through the class.module.classLoader. However, the situation changes when dealing with a Tomcat server, as its classloader has a getResources accessor that enables the continuation of the reference chain. As a result, it becomes possible to manipulate and set additional properties of a class.

The payload employed in the most famous exploit targets Tomcat servers and utilizes a well-known technique dating back to 2014 via manipulating the logging properties[3] of the Tomcat 9 server through the features of WebAppClassLoaderBase[4]. With the exploit, one can redirect the logging logic into a specific directory and inject arbitrary content into it. Therefore, creating a webshell is possible.

A word about Tomcat Valves

Apache Tomcat’s Valve pipeline provides a flexible and extensible mechanism for request/response processing. Each stage in the pipeline is represented by a component called a “Valve” that performs specific tasks or modifications on the request or response. They can be configured in the server’s configuration files (usually in the server.xml file) and are responsible for intercepting requests and responses as they pass through the pipeline and can perform various operations such as authentication, logging, compression, encryption, and more. So when processing requests and responses, the operations defined in the pipelines will be executed.

Valve implementations in Apache Tomcat include the AccessLogValve Valve for logging requests that are utilized in this attack path.

Details of the exploit

Let’s put together what we already know about the exploitation:

  • By sending data to a controller, Spring tries to convert that into a POJO so an object is constructed.
  • Spring supports the binding of nested fields.
  • The classloader object graph path can’t be overridden, but
  • rom Java 9 the Class object also exposes the getModule() method that has a classLoader (class.module.classLoader),
  • therefore we have access to the class hierarchy of the loaded classes.
  • In the case of Tomcat servers, there is a getResources accessor (class.module.classLoader.resources),
  • which can be used to access the Tomcat Valve pipeline (class.module.classLoader.resources.context.parent.pipeline).

With the attack, we exploit this mechanism by overriding Tomcat’s AccessLogValve object so creating a new logging mechanism.

But let’s see what will be new content of the attributes of the logging Valve (class.module.classLoader.resources.context.parent.pipeline).

Some explanation for the logging attributes used in the exploit:

  • pattern: The pattern used is a formatting layout that specifies the fields to extract from the request and log in the response. It demonstrates the retrieval of headers such as 'c2', 'c1', and 'suffix' from the incoming headers. The substitution occurs by replacing the placeholders, denoted as %{name_of_header}i, with the corresponding values of the headers. With this field, we can then write an arbitrary payload into the created .jsp file. The content you can see is a common Java webshell payload.
  • suffix: The suffix to add to the end of each log file name. The extension of the file that will be written is .jsp.
  • directory: The directory path, whether absolute or relative, specifies where the file should be created. When not specified, it’s webapps/ROOT what is the default path in a Tomcat installation. In short, it is for overwriting the directory where Tomcat logging files are stored locally on the server.
  • prefix: this string is added to the start of each log file's name. If not specified, the default value is "access_log".
  • fileDateFormat: enables the inclusion of a customized timestamp in the log file name. In this scenario, it is intentionally left empty to avoid any additional extensions in the JSP webshell. The empty value indicates the absence of the default timestamp format.

By creating a JSP file containing a malicious payload within the root of the application folder, we take advantage of the automatic execution of JSP files by Tomcat. This allows us to navigate to the JSP file in a web browser and potentially execute the payload. This scenario leads to Remote Code Execution (RCE), where an attacker gains the ability to execute arbitrary code on the target system through the compromised JSP file.

Debugging

Setting up the debugger

The following part is added to the Dockerfile in order to debug remotely with IntelliJ Idea:

ENV CATALINA_OPTS="-Xdebug -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
EXPOSE 5005

If the image is running, then select Run / Edit Configurations in the IntelliJ menu.

Then, click on the “+” icon in the top left corner and select Remote JVM Debug. You should see this window:

Let’s set up a breakpoint to see how a “normal” request is processed! (If you are not familiar with debugging, consider breakpoint as a specific line of code where the execution of a program is paused, allowing the developer to inspect variables, evaluate expressions, and analyze program flow.) You can insert a breakpoint by clicking on the space next to the line number in IntelliJ.

We are curious about the POJO instantiation process so let’s put this to the DemoObject class header!

As a next step, run the app. As previously mentioned, you can build and run the image with this command:

docker build . -t spring4shell && docker run -p 80:8080 -p 5005:5005 spring4shell

Then, click on the debug icon!

In the console, you should see this message: Connected to the target VM, address: 'localhost:5005', transport: 'socket'.

As a next step, let’s see what’s happening under the hood when sending a simple POST request!

curl -k -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data-binary 'data' http://localhost/spring4shell/hello -v

After sending the POST request you should immediately see the call stack in the Debugger console:

We mentioned that through the instantiation process, an attacker can manipulate the AcessLogValve attributes. Let’s see what’s happening in the AbstractAccessLogValve class (the method invoke of this class will be around the end of the call stack).

The values of the attributes of AccessLogValve are the default ones:

From this point things are going to be (even) more interesting, I promise!

Restart the debug session by clicking on the Stop button (red rectangle) and click on the debug button again! Then, run the exploit with this command:

./exploit.sh --url http://localhost/spring4shell/hello --dir spring4shell

Resume the first pause since the overwriting of the Valve log attributes happening in the second request only as the exploit script will also note.

Also, resume the next pause triggered by the second request since at that point the attributes are still not set. At the time of the third pause, you will see that that AccessLogValve object’s attributes have changed to what was defined in the exploit payload:

With the third and fourth requests of the exploit we send the web shell-writing packet and reset the log variables to prevent future writes into the file. After that, we can run any command on the webserver remotely:

Patch diffing

A patch related to the Spring4shell vulnerability is now available as of March 31st, 2022 in the Spring versions 5.3.18 and 5.2.20. The comparison you can see below is between Spring versions 5.2.19 and 5.2.20[5].

Actually, the bug resides within the getCachedIntrospectionResults method, enabling unauthorized access to objects by injecting their classnames through HTTP requests, particularly when dealing with specific object classes. The code in question[6]:

PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
if (Class.class == beanClass &&
("classLoader".equals(pd.getName()) || "protectionDomain".equals(pd.getName()))) {
// Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those
continue;
  }
...

This code aims to restrict access to certain object graph paths such as Class.getClassLoader() and Class.getProtectionDomain(), but it fails to account for the introduction of the Class.getModule() method, which allows attackers to access the Module and subsequently its ClassLoader. In the version update, security vulnerabilities related to the "class.classLoader" and "class.protectionDomain" logic has been addressed. The main change was to restrict access to most of the Class object properties, including the module too:

PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
if (Class.class == beanClass && (!"name".equals(pd.getName()) && !pd.getName().endsWith("Name"))) {
// Only allow all name variants of Class properties
continue;
}
  if (pd.getPropertyType() != null && (ClassLoader.class.isAssignableFrom(pd.getPropertyType()) || ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) {
// Ignore ClassLoader and ProtectionDomain types - nobody needs to bind to those
continue;
}
...

The improved logic now ensures more secure child property access. Specifically, only the “name” variants of class properties are permitted, and the binding of ClassLoader and ProtectionDomain types are no longer allowed.

These changes enhance the overall security of the system.

Here is a diff view of the patch:

Mitigation

Spring Framework versions 5.3.18 and 5.2.20 were released on March 31, 2022, following the confirmation of a zero-day vulnerability by the Spring team. These updates were specifically developed to fix the vulnerability so no other actions are not required than updating to Spring Framework 5.3.18 and 5.2.20 or higher.

Additionally, upgrading to Apache Tomcat 10.0.20, 9.0.62, or 8.5.78 offers sufficient security for older applications running on Tomcat with an unsupported Spring Framework version.

If upgrading Apache Tomcat or the Spring Framework is not an option for some reason, one can downgrade to Java 8 as a workaround too.

Several manual workarounds were also developed, but their relevance was greater before the release of the versions containing the fix. However, you can learn about them on the official Spring blog related to fixing this vulnerability[7].

Final thoughts

In conclusion, the discovery of the RCE vulnerability known as Spring4shell has shed light on the critical importance of maintaining secure coding practices and regularly updating frameworks. The exploit’s ability to execute remote commands highlights the potential risks posed by such vulnerabilities.

The Spring community promptly responded to the discovery, with the Spring team swiftly confirming the vulnerability and releasing fixes in the form of Spring Framework versions 5.3.18 and 5.2.20. These updates address the issue, reinforcing the significance of keeping software up to date and applying patches promptly.

Remember, security is an ongoing effort that requires the collaboration and dedication of developers, security experts, and the entire software community. Together, we can safeguard our applications and build a more secure digital landscape. Stay proactive, stay updated, and stay secure!

Resources

References

--

--

vsociety
vsociety

Written by vsociety

vsociety is a community centered around vulnerability research

No responses yet