长亭百川云 - 文章详情

CVE-2024-4956 Nexus Repository 3 任意文件读取调试分析

黑伞安全

82

2024-07-13

漏洞概述

2024年5月,Nexus Repository官方Sonatype发布了新补丁,修复了一处路径穿越漏洞CVE-2024-4956。经分析,该漏洞可以通过特定的路径请求来未授权访问系统文件,进而可能导致信息泄露。该漏洞无前置条件且利用简单。

  • • 漏洞成因Nexus Repository仅依赖Jetty自带的方法进行请求路径的安全检查,而未进行深入的验证,导致攻击者可以利用路径穿越攻击访问文件系统上的任意位置。

  • • 漏洞影响成功利用这一漏洞的攻击者可以读取Nexus Repository服务器上的任意文件,这可能包括配置文件、数据库备份以及其他敏感数据。此外,特定情况下如果攻击者能够进一步利用服务器上的其他配置或漏洞,可能会完全控制受影响的服务器。

环境搭建

  • • 下载源码:nexus-public-release-3.68.0-04.zip

  • • vulhub启动漏洞环境,idea远程调试

调试分析

根据官方的临时修复方案,删除(basedir)/etc/jetty/jetty.xml <Set name="resourceBase"><Property name="karaf.base"/>/public</Set>这一行 整个xml

<New id="NexusHandler" class="org.sonatype.nexus.bootstrap.jetty.InstrumentedHandler">  
    <Arg>  
      <New id="NexusWebAppContext" class="org.eclipse.jetty.webapp.WebAppContext">  
        <Set name="descriptor"><Property name="jetty.etc"/>/nexus-web.xml</Set>  
        <Set name="resourceBase"><Property name="karaf.base"/>/public</Set>  
        <Set name="contextPath"><Property name="nexus-context-path"/></Set>  
        <Set name="throwUnavailableOnStartupException">true</Set>  
        <Set name="configurationClasses">  
          <Array type="java.lang.String">  
            <Item>org.eclipse.jetty.webapp.WebXmlConfiguration</Item>  
          </Array>  
        </Set>  
      </New>  
    </Arg>  
  </New>

断点直接下在WebResourceServiceImpl补丁删除的那块代码上⾯,这⾥以/robots.txt路由为例⼦

跟进getResource

再次跟进,来到了ContextHandlergetResource⽅法,这里的_baseResource就是jetty.xml⾥配置的public⽬录

继续跟进addPath方法,这里会调用URIUtil.canonicalPath

创建了⼀个PathResource对象准备返回

实际的uri拼接在于URIUtil.addPath函数中,注意这⾥的encodePath参数对path⼜进⾏了⼀波编码

【重点】canonicalPath 处理逻辑

这里我们直接拿poc来直接调试分析一下

GET /%2F%2F%2F%2F%2F%2F%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd

然后我们直接走到URIUtil.canonicalPath方法来分析

canonicalPath方法代码如下所示,增加了详细的注释

public static String canonicalPath(String path) {  
    // 检查路径是否为空或空字符串  
    if (path != null && !path.isEmpty()) {  
        boolean slash = true; // 标志当前字符是否是'/'  
        int end = path.length(); // 获取路径的长度  
  
        int i;  
        label68:  
        // 遍历路径中的每个字符  
        for(i = 0; i < end; ++i) {  
            char c = path.charAt(i); // 获取路径中的当前字符  
            switch (c) {  
                case '.':  
                    // 如果当前字符是'.'且前一个字符是'/'  
                    if (slash) {  
                        break label68; // 跳出循环  
                    }  
                    slash = false; // 当前字符不是'/'  
                    break;  
                case '/':  
                    slash = true; // 当前字符是'/'  
                    break;  
                default:  
                    slash = false; // 当前字符不是'/'  
            }  
        }  
  
        // 如果遍历到路径的末尾  
        if (i == end) {  
            return path; // 返回原路径  
        } else {  
            StringBuilder canonical = new StringBuilder(path.length()); // 创建一个新的字符串构建器  
            canonical.append(path, 0, i); // 将原路径的前i个字符复制到新的字符串构建器中  
            int dots = 1; // 初始化dots为1,用来计数'.'字符  
            ++i; // 继续遍历路径  
  
            for(; i < end; ++i) {  
                char c = path.charAt(i); // 获取路径中的当前字符  
                switch (c) {  
                    case '.':  
                        // 如果当前字符是'.'  
                        if (dots > 0) {  
                            ++dots; // 增加dots计数  
                        } else if (slash) {  
                            dots = 1; // 如果前一个字符是'/',dots置为1  
                        } else {  
                            canonical.append('.'); // 否则将'.'添加到新的字符串构建器中  
                        }  
                        slash = false; // 当前字符不是'/'  
                        continue;  
                    case '/':  
                        // 如果当前字符是'/'  
                        if (doDotsSlash(canonical, dots)) {  
                            return null; // 如果doDotsSlash返回true,返回null  
                        }  
                        slash = true; // 当前字符是'/'  
                        dots = 0; // dots置为0  
                        continue;  
                }  
  
                // 将前面的'.'字符添加到新的字符串构建器中  
                while(dots-- > 0) {  
                    canonical.append('.');  
                }  
  
                canonical.append(c); // 添加当前字符到新的字符串构建器中  
                dots = 0; // dots置为0  
                slash = false; // 当前字符不是'/'  
            }  
  
            // 检查剩余的'.'字符  
            if (doDots(canonical, dots)) {  
                return null; // 如果doDots返回true,返回null  
            } else {  
                return canonical.toString(); // 返回处理后的路径字符串  
            }  
        }  
    } else {  
        return path; // 如果路径为空或空字符串,直接返回  
    }  
}  
  
// 处理路径中的连续'.'字符和'/'字符  
private static boolean doDotsSlash(StringBuilder canonical, int dots) {  
    if (dots == 2) {  
        // 如果dots为2,表示路径中有"..",需要返回上一级目录  
        int length = canonical.length();  
        if (length == 0) {  
            return true; // 如果字符串构建器为空,返回true表示路径无效  
        }  
  
        int slash = canonical.lastIndexOf("/"); // 获取最后一个'/'的位置  
        if (slash < 0) {  
            canonical.setLength(0); // 如果没有'/',清空字符串构建器  
        } else {  
            canonical.setLength(slash); // 否则将字符串构建器的长度设置为最后一个'/'的位置  
        }  
    } else if (dots == 1) {  
        // 如果dots为1,表示路径中有".",忽略  
    } else if (dots > 2) {  
        // 如果dots大于2,将'.'添加到字符串构建器中  
        while(dots-- > 0) {  
            canonical.append('.');  
        }  
    }  
    return false; // 返回false表示路径有效  
}  
  
// 处理路径中剩余的'.'字符  
private static boolean doDots(StringBuilder canonical, int dots) {  
    if (dots == 2) {  
        // 如果dots为2,表示路径中有"..",需要返回上一级目录  
        int length = canonical.length();  
        if (length == 0) {  
            return true; // 如果字符串构建器为空,返回true表示路径无效  
        }  
  
        int slash = canonical.lastIndexOf("/"); // 获取最后一个'/'的位置  
        if (slash < 0) {  
            canonical.setLength(0); // 如果没有'/',清空字符串构建器  
        } else {  
            canonical.setLength(slash); // 否则将字符串构建器的长度设置为最后一个'/'的位置  
        }  
    } else if (dots == 1) {  
        // 如果dots为1,表示路径中有".",忽略  
    } else if (dots > 2) {  
        // 如果dots大于2,将'.'添加到字符串构建器中  
        while(dots-- > 0) {  
            canonical.append('.');  
        }  
    }  
    return false; // 返回false表示路径有效  
}

canonicalPath大致处理逻辑:

处理斜杠:

  • • 遍历前8个字符(全是斜杠),slash始终为 true

遇到第一个点号

  • • 第9个字符是点号(.),slash为 true,跳出循环。此时 i = 8

**初始化 StringBuilder**:

  • • canonical.append(path, 0, i) 将前8个斜杠添加到 canonical,此时 canonical = "////////",i=9,开始遍历剩余的路径字符。

遇到点号和斜杠

  • • 第9到26个字符处理过程中,路径包含多个 ../../ 模式。每次遇到 .. 后的斜杠会调用 doDotsSlash 函数。

**调用 doDotsSlash 处理 ..**:

  • • 每次遇到 .. 和随后的斜杠,会返回上一级目录。第一次遇到..的时候,dots = 2,调用 doDotsSlash

  • • canonical.length() = 8

  • • canonical.lastIndexOf("/") = 7(最后一个斜杠的位置)

  • • canonical.setLength(7),结果:canonical = "///////"

  • • 以此类推,处理 ../../../../../../../,多次调用 doDotsSlash 函数返回上级目录,逐步移除斜杠。最终返回的路径为 /etc/passwd

总结规律:⼀个../可以消去⼀个/

柳暗花明

再回头来看,只要canonicalPath不为null, subpath是直接交给PathResource对象的,然后addPath拼接url

其实还有一些细节也没讲到,大家可以自己多调试一些payload,了解最终的poc是怎么形成的

欢迎关注不懂安全⬇️

> 时间会告诉你,坚持的意义。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2