Spring Cloud Config微服务架构的配置中心,为各个不同服务提供了一个中心化的外部配置。不适用本地配置是因为集群间不方便管理,需要一个平台,通常是从github等平台拉取配置。之前在字节工作,他们用的TCC平台,也是一个类似Spring Cloud Config的配置中心
Spring Cloud Config分为服务端和客户端两部分:
这个漏洞不是RCE,是目录遍历,不算高危
从github下载spring-cloud-config模块:https://github.com/spring-cloud/spring-cloud-config/archive/v2.1.5.RELEASE.zip
导入IDEA其中的spring-cloud-config-server模块,Maven+SpringBoot项目
修改配置文件src/main/resources/configserver.yml,主要是设置profiles.active为native,设置search-locations为任意文件夹
xxxxxxxxxxinfo componentConfig Serverspring application nameconfigserver autoconfigure.excludeorg.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration jmx default_domaincloud.config.server profiles activenative cloud config server native search-locationsfile:///D:/Codeserver port8888management context_path/admin从ConfigServerApplication类启动,访问localhost:8888
手动在D:/Code下创建一个key.txt,尝试读取这个文件
Payload:
http://127.0.0.1:8888/1/1/..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)Code/key.txt
http://127.0.0.1:8888/1/1/..%28_%29..%28_%29..%28_%29..%28_%29..%28_%29..%28_%29..%28_%29..%28_%29Code/key.txt(编码后)

Config Server通过路径/{name}/{profile}/{label}/{path}对外提供配置文件,POC会通过路由到这个接口,参考org\springframework\cloud\config\server\resource\ResourceController.java
xxxxxxxxxx("/{name}/{profile}/{label}/**")public String retrieve( String name, String profile, String label, ServletWebRequest request, (defaultValue = "true") boolean resolvePlaceholders) throws IOException { String path = getFilePath(request, name, profile, label); return retrieve(request, name, profile, label, path, resolvePlaceholders);}下断点查看这一步的name和profile等变量分别是什么

Payload:
/1/1/..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)Code/key.txt
不难看出getFilePath方法作用是解析**为key.txt
xxxxxxxxxxprivate String getFilePath(ServletWebRequest request, String name, String profile, String label) { String stem; if (label != null) { stem = String.format("/%s/%s/%s/", name, profile, label); } else { stem = String.format("/%s/%s/", name, profile); } String path = this.helper.getPathWithinApplication(request.getRequest()); path = path.substring(path.indexOf(stem) + stem.length()); return path;}最后调用retrieve方法
xxxxxxxxxxsynchronized String retrieve(ServletWebRequest request, String name, String profile, String label, String path, boolean resolvePlaceholders) throws IOException { name = resolveName(name); label = resolveLabel(label); Resource resource = this.resourceRepository.findOne(name, profile, label, path); ......其中的resolveName和resolveLabel方法作用一致,都是将(_)换成/
xxxxxxxxxxprivate String resolveName(String name) { if (name != null && name.contains("(_)")) { // "(_)" is uncommon in a git repo name, but "/" cannot be matched // by Spring MVC name = name.replace("(_)", "/"); } return name;}所以到达findOne方法这里:
xxxxxxxxxxpublic 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; } } } } } ......this.service.getLocations方法应该是调用配置文件并重新拼接,最终得到两个路径,其中第1(2)个是我们的payload

后面的getProfilePaths方法是根据profile构造文件路径。这个主要是参照日常习惯,比如开发配置文件会命名为confg-dev.yaml这样

其实程序做了非法路径的判断:isInvalidPath,不过它校验的path,而我们的path是key.txt,不是违法的路径
xxxxxxxxxxprotected 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;}另一个编码校验自然也无效
xxxxxxxxxxprivate 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;}最后通过this.resourceLoader.getResource(location).createRelative(local);加载了本地文件,这个没必要跟踪,已经确认了过程
补丁:https://github.com/spring-cloud/spring-cloud-config/commit/651f458919c40ef9a5e93e7d76bf98575910fad0
补丁对location进行了验证,而不只是简单地对path做验证
实际中也确实应该对最后一步的输入进行判断,而不只是对过程进行判断
xxxxxxxxxxprivate boolean isInvalidLocation(String location) { boolean isInvalid = location.contains("..");
if (isInvalid && logger.isWarnEnabled()) { logger.warn("Location contains \"..\""); } return isInvalid;}