CVE-2020-5405 Spring Cloud Config 路径穿越漏洞浅析
2020-09-29 20:34:09 Author: xz.aliyun.com(查看原文) 阅读量:316 收藏

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,下载地址为:

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-activenative,设置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,应配置文件环境。

  • labelgit分支名。
  • **,通配子目录。

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

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)

先跟进处理nameresolveName函数

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为空

然后对这个集合再次遍历,取出元素后通过sInvalidPathisInvalidEncodedPath进行安全检查,关注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.resourceLoaderAnnotationConfigServletWebServerApplicationContext类加载器,继续通过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

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/


文章来源: http://xz.aliyun.com/t/8303
如有侵权请联系:admin#unsafe.sh