CVE-2019-3799 Spring Cloud Config目录遍历漏洞

官方描述:https://spring.io/blog/2019/04/17/cve-2019-3799-spring-cloud-config-2-1-2-2-0-4-1-4-6-released

We have released Spring Cloud Config 2.1.2, 2.0.4, and 1.4.6 to address CVE-2019-3799: Directory Traversal with spring-cloud-config-server. Please review the information in the CVE report and upgrade immediately.

release中找到修复前的版本2.1.1代码 https://github.com/spring-cloud/spring-cloud-config/releases/tag/v2.1.1.RELEASE 用于复现

本次更新的代码路径为spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/GenericResourceRepository.java,commit地址为 https://github.com/spring-cloud/spring-cloud-config/commit/3632fc6f64e567286c42c5a2f1b8142bfde505c2

修复分析

补丁主要是在findOne()方法中增加了传入参数path的判断

diff-2.1.1-findOne.png

isInvalidPath方法用于判断路径中是否存在WEB-INFMETA-INF:/../,如果存在则返回true,导致if (!isInvalidPath(local))为假。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}

isInvalidEncodedPath方法用于判断路径中是否存在%,若存在则再进行一次URL解码再调用isInvalidPath方法。

第二个和第三个if中的processPath方法是将\替换成/,再清除连续重复的/

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

利用分析

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

既然是findOne()方法中增加了路径分析,表明之前未做路径防护可能导致任意文件读取漏洞产生。回溯findOne()方法就能找到漏洞的利用点。查看findOne()方法的调用信息(在方法处右键点击Find Usages),可以看到有12处相关引用,由于限制在path参数,于是主要寻找path参数可控的点。

find-findOne-usages.png

找到src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java中调用了findOne()方法,且path可控。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@RequestMapping("/{name}/{profile}/{label}/**")
public String retrieve(@PathVariable String name, @PathVariable String profile,
@PathVariable String label, ServletWebRequest request,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws IOException {
String path = getFilePath(request, name, profile, label);
return retrieve(request, name, profile, label, path, resolvePlaceholders);
}

...

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;
}
}

path参数为@RequestMapping("/{name}/{profile}/{label}/**")**的部分,也就是uri第三个标签后的全部内容。注意此处传入的label必须为master,具体原因在CVE-2020-5405中会讲到。

传入GET /name/profile/master/..%252F..%252F..%252F..%252F..%252F..%252Ftmp%252Ftest.txt时,path参数为..%252F..%252F..%252F..%252F..%252F..%252Ftmp%252Ftest.txt

debug-retrieve

FileUrlResource对象中的relativePath..%2F..%2F..%2F..%2F..%2F..%2Ftmp%2Ftest.txt

debug-FileUrlResource

path解码后的路径,路径回溯正好变成/tmp/test.txt

debug-file

findOne()方法执行完成之后,返回Resource对象。Resource是Spring框架中用于访问低级资源的一个抽象接口,常用来读取配置文件。

debug-return-file

根据findOne()返回的Resource对象,读取到的文件内容。

debug-get-text

复现

IDEA导入程序后,先等待maven自动安装依赖。依赖安装完成后,找到spring-cloud-config-server中src/main/java/org/springframework/cloud/config/server/ConfigServerApplication.java,点击ConfigServerApplication类右边的运行,启动程序即可,端口默认为8888,想要修改端口的话可在src/main/resources/configserver.yml中修改。

run-application.png

程序运行起来之后访问http(s)://ip:port/foo/label/master/..%252f..%252f..%252f..%252fetc%252fpasswd得到以下结果

poc-request

不二次编码的效果如下,因为不二次编码传到代码前会自动URL解码一次,变成/foo/label/master/../../../../etc/passwd,等效于直接访问/../etc/passwd,tomcat会把它识别成一个错误的URI,请求不会传到ResourceController中,也就不会进行后续findOne()方法的调用。

not-double-encode