Spring Cloud Config,为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
Spring Cloud Config分为服务端和客户端两部分:
git
来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git
客户端工具来方便的管理和访问配置内容。CVE-2020-5405,Spring Cloud Config允许应用程序通过spring-cloud-config-server
模块使用任意配置文件。 恶意用户或攻击者可以发送精心构造的包含(_)
的请求进行目录穿越攻击。
影响版本:
下载官方Spring Cloud Config,具体版本versions 2.1.5.RELEASE
,下载地址为:
https://github.com/spring-cloud/spring-cloud-config/archive/v2.1.5.RELEASE.zip
导入IDEA项目
修改配置文件src/main/resources/configserver.yml
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-active
为native
,设置search-locations
为任意文件夹。
主文件入口位置为org.springframework.cloud.config.server.ConfigServerApplication
,运行spring-cloud-config-server
模块,环境开启成功运行在127.0.0.1:8888
。
POC
http://127.0.0.1:8888/1/1/..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)etc/passwd
URL编码变形
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
结果
目录穿越成功,问题出现在Spring Cloud Config服务端,简单的看是将/
替换成为(_)
。
查看官方文档:
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)
解析下路由的结构
name
,应仓库名称。
profile
,应配置文件环境。
label
,git
分支名。**
,通配子目录。打好断点,查看被解析后的关键变量:
request
为该次请求对象, name
和profile
对应解析为1
,label
对应解析为..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)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)
先跟进处理name
的resolveName
函数
org.springframework.cloud.config.server.resource.ResourceController#resolveName
替换name
中存在的(_)
,name经过处理后不发生变化,继续跟进resolveLabel
。
org.springframework.cloud.config.server.resource.ResourceController#resolveLabel
..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)etc
经过替换之后变为../../../../../../../../../etc
,然后将几个处理过的变量传入并跟进this.resourceRepository.findOne
函数。
org.springframework.cloud.config.server.resource.GenericResourceRepository#findOne
@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
协议的绝对路径地址且为有两个元素的素组
接着通过for
循环对locations
数组元素进行遍历,与POC相关的是第一号元素,取出后传入getProfilePaths
函数。
org.springframework.cloud.config.server.resource.GenericResourceRepository#getProfilePaths
创建了一个集合包含两个元素,首先包含原本的passwd
,还有第二个根据file + "-" + profile + ext
拼接而成的元素,此时profile为1,ext为空
然后对这个集合再次遍历,取出元素后通过sInvalidPath
和isInvalidEncodedPath
进行安全检查,关注1号元素passwd
即可。
org.springframework.cloud.config.server.resource.GenericResourceRepository#isInvalidPath
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
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.resourceLoader
为AnnotationConfigServletWebServerApplicationContext
类加载器,继续通过this.resourceLoader.getResource(location).createRelative(local);
加载资源,最终file为:
最终作为结果进行层层返回,完成任意文件读取。
git地址
https://github.com/spring-cloud/spring-cloud-config/commit/651f458919c40ef9a5e93e7d76bf98575910fad0
org.springframework.cloud.config.server.resource.GenericResourceRepository#isInvalidLocation
在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