Spring Boot Actuator 是 Spring Boot 提供的一个管理和监控 Spring Boot 应用程序的插件。Actuator 可以提供有关应用程序的健康状况、度量、日志记录和其他信息,可以通过 HTTP 或 JMX 等不同方式进行访问。
/env 是 Spring Boot Actuator 的一个端点,用于显示当前应用程序的环境属性。该端点返回一个 JSON 格式的响应,其中包含有关应用程序环境的详细信息,例如操作系统、Java 运行时环境、应用程序配置属性等等。
默认情况下,actuator的env端点是不支持POST请求修改操作的。但是可通过配置开启env端点的post请求,参考
https://spring.io/blog/2020/03/05/spring-cloud-hoxton-service-release-3-sr3-is-available:
PS:actuator 1.x只需要引入对应的springcloud即可默认开启了。
在引入springcloud后,会依赖一个组件叫 SnakeYAML 其可以触发yaml反序列化,利用actuator的/env POST 功能可以将spring.cloud.bootstrap.location属性的值为恶意yml配置文件的地址,使用/refresh接口刷新后即可触发远程加载yaml 实现urlclassloader达到RCE的效果。
针对SpringBoot-Actuator-SnakeYAML-RCE的情况,很多时候会选择关闭掉env端点来规避风险。
在actuator 1.x,/env端点可以由如下属性进行关闭:
endpoints.env.enabled=false
在spring boot 2.0以后,actuator默认只开启了info和health端点,要想使用其他的端点,需要在application.yml相关配置文件中打开,并且所有的端点都以默认的路径/actuator/开始,如果需要独立关闭/env端点的话可以通过如下属性进行操作:
management.endpoint.env.enabled=false
这里存在一个误区,在actuator 1.x中,很多时候大家
都认为endpoints.env.enabled=false后就env POST
端点就没有暴露出去了,规避了风险,实际上简单的
通过配置关闭env端点并不能解决问题。
针对前面的情况,看一下具体的原理(在spring boot 2.0以后,actuator默认只开启了info和health端点,这里只讨论1.x的情况):
以spring-boot-actuator-1.5.1.RELEASE版本为例:
首先通过actuator访问/mappings查看env端点具体的方法调用,可以看到是通过
endpointHandlerMapping进行处理的:
访问/beans节点,查看处理类
endpointHandlerMapping的具体实现:
可以看到endpointHandlerMapping对应的类为
org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping
初始化的configuration类为
org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.class:
首先查看
EndpointWebMvcManagementContextConfiguration的具体实现
可以看到其会通过调用endpointHandlerMapping方法来处理,这里首先会获取容器中的MvcEndpoint接口实现类,然后实例化EndpointHandlerMapping:
MvcEndpoints初始化代码如下:
查找spring的beanfactory中所有的MvcEndpoint类和Endpoint类
如果某个MvcEndpoint的getEndpointType方法的返回结果和某个Endpoint的class相冲突,则选取MvcEndpoin
最后将查找选取的结果放在MvcEndpoints的endpoints方法中并返回
通过断点调试,可以看到当前环境endpoints有21个:
继续调试,后面会调用对应Endpoint的构造方法来实现映射
(EnvironmentMvcEndpoint也在21个endpoints里):
以EnvironmentMvcEndpoint为例,查看具体的实现:
org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint#value
这个方法实际上就是/env/{name:.*}这个路由的具体调用:
其中@ActuatorGetMapping
就是@RequestMapping的封装,被该注解标注的方法,其请求方式为get,产生的数据格式为
application/vnd.spring-boot.actuator.v1+json
和application/json
具体定义如下:
查看EnvironmentMvcEndpoint的构造方法,直接调用直接父类构造函数
(EnvironmentMvcEndpoint的父类是
EndpointMvcAdapter):
在EndpointMvcAdapter中,
org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter#invoke实际上就是/env路由的具体调用:
EndpointMvcAdapter,它代理了MvcEndpoint,实现其invoke方法。
这里的delegate是EnvironmentEndpoint,所以实际调用时,最终调用的是
org.springframework.boot.actuate.endpoint.EnvironmentEndpoint#invoke:
到这里大概可以知道EnvironmentMvcEndpoint实际上就是把EnvironmentEndpoint通过HTTP服务暴露,然后通过/env和/env/{name:.*}两个请求路由进行访问。
前面提到会调用对应Endpoint的构造方法来实现映射,对应的方法里有一个注解
@ConditionalOnEnabledEndpoint:
该注解会通过
org.springframework.boot.actuate.condition.OnEnabledEndpointCondition
类进行处理。
首先是getMatchOutcome方法:
主要做了如下处理:
获得@ConditionalOnEnabledEndpoint配置的Endpoint名字和
enabledByDefaultenabled的值
调用determineEndpointOutcome方法,获得endpoints.{name}.enabled的配置并返回,例如env端点的话会读取endpoints.env.enabled的值
读取endpoints.enabled的配置,如果没有配置的话,则默认匹配
也就是说@ConditionalOnEnabledEndpoint注解满足如下条件时或生效:
endpoints. env.enabled=true
endpoints.enabled=true
未配置
endpoints.enabled,endpoints.env.enabled时默认生效
同样是之前的环境,在application.properties进行如下配置关闭env端点:
endpoints.env.enabled=false
通过断点调试,可以看到当前环境endpoints 从21个减少到了20个,EnvironmentMvcEndpoint没有了:
根据前面的分析,通过@ActuatorGetMapping注解仅仅实现了GET请求的调用,并未实现POST。看看具体的env POST是如何实现的。
同样的通过actuator访问/mappings查看env端点具体的方法调用,可以看到是通过
org.springframework.cloud.context.environment.EnvironmentManagerMvcEndpoint#value处理的,查看具体的实现:
以spring-cloud-context-1.1.3为例:
org.springframework.cloud.context.environment.EnvironmentManagerMvcEndpoint#value方法通过@RequestMapping标记并且限制了只能通过POST请求:
同时EnvironmentManagerMvcEndpoint实现了MvcEndpoint接口
在MvcEndpoints初始化时也能获取到:
EnvironmentManagerMvcEndpoint是在
org.springframework.cloud.autoconfigure.LifecycleMvcEndpointAutoConfiguration中注入
这里是通过@ConditionalOnProperty注解来判断的(@ConditionalOnProperty可以通过配置文件中的属性值来判定是否被注入,这里是endpoints.env.post.enabled,matchIfMissing属性为true时,配置文件中缺少对应的value或name的对应的属性值,也会注入成功):
到这里其实已经大概知道具体的原因了。实际上env POST和env GET在endpointHandlerMapping处理时并不是同一个endpoint。
所以当通过endpoints.env.enabled=false关闭env GET时,env POST 端点
(EnvironmentManagerMvcEndpoint)仍因为默认配置注入到环境中。
根据前面的分析,这里尝试在application.properties中进行如下配置:
endpoints.env.post.enabled=false
但是env GET可以正常访问,验证了前面的猜想:
查看调试信息,EnvironmentManagerMvcEndpoint没有注入,但是代表env GET实现的EnvironmentMvcEndpoint注入成功并且获取到了,所以env GET可以访问:
对于actuator 2.x生态,设计上会有区别。
以spring-cloud-context-2.2.1.RELEASE为例,其env POST的实现主要在
WritableEnvironmentEndpointAutoConfiguration:
这里不再是EnvironmentManagerMvcEndpoint,而是
WritableEnvironmentEndpoint:
而POST的实现主要是通过@EndpointWebExtension注解扩展现有的Endpoint(ReadOperation、@WriteOperation和@DeleteOperation每个注解的方法都有对应操作对象,根据操作对象、Endpoint对象、Endpoint名称在RequestMappingInfo进行处理,然后对外提供服务):
那么在2.x中,实际上只需要关闭env端点,即可禁用env POST了,例如如下配置,首先暴露endpoint,然后禁用env:
management.endpoints.web.exposure.include=*
此时再次尝试请求env POST,返回404 status:
根据前面的分析,env端点POST支持主要是通过spring-cloud-context组件控制的。在1.x中需要额外的注意。
当引入了spring-cloud-starter-config或
spring-cloud-context 大于等于 2.2.2.RELEASE时:
<dependency>
若未配置
management.endpoint.env.post.enabled=true
则不支持post请求,此时无影响。
但当版本为2.2.1.RELEASE及以下时候,则可以直接请求POST:
<dependency>
此时如果需要关闭,通过配置对应的属性进行处理:
endpoints.env.post.enabled=false
management.endpoint.env.post.enabled=false
同时关闭env POST和GET后,访问会返回404,达到预期的效果,从一定程度规避了env POST端点的风险:
往期推荐
原创 | 2023年第八届上海市大学生网络安全大赛 / 磐石行动 漏洞挖掘 Workthrough(上)