Apache Shiro 身份验证绕过漏洞 (CVE-2020-1957)

发布于 2021-09-15  382 次阅读


Shiro是一个功能强大且易于使用的 Java 安全框架,可执行身份验证、授权、加密和会话管理。在带有 Spring 动态控制器的 1.5.2 之前的 Apache Shiro 版本中,攻击者可以使用 ..;绕过目录认证。

环境搭建

漏洞环境:使用 Spring 2.2.2 和 Shiro 1.5.1 启动应用程序,环境启动后,访问 http://your-ip:8080查看主页。

本应用中 URL 权限的配置如下。

@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    chainDefinition.addPathDefinition("/login.html", "authc"); // need to accept POSTs from the login form
    chainDefinition.addPathDefinition("/logout", "logout");
    chainDefinition.addPathDefinition("/admin/**", "authc");
    return chainDefinition;
}

会对admin所有的页面都会进行权限校验。测试结果如下:

访问首页:

直接请求管理页面 /admin/无法访问,将被重定向到登录页面。

访问admin时使用burp抓包:

漏洞分析

绕过演示

构造恶意请求 /xxx/..;/admin/绕过身份验证检查并访问管理页面,在shiro的1.5.1及其之前的版本都可以完美地绕过权限检验,如下所示:

绕过原理分析

我们需要分析我们请求的URL在整个项目的传入传递过程。在使用了shiro的项目中,是我们请求的URL(URL1),进过shiro权限检验(URL2), 最后到springboot项目找到路由来处理(URL3)

漏洞的出现就在URL1,URL2和URL3 有可能不是同一个URL,这就导致我们能绕过shiro的校验,直接访问后端需要首选的URL。本例中的漏洞就是因为这个原因产生的。

http://localhost:8080/xxxx/..;/admin/index 为例,一步步分析整个流程中的请求过程。

protected String getPathWithinApplication(ServletRequest request) {
    return WebUtils.getPathWithinApplication(WebUtils.toHttp(request));
}

public static String getPathWithinApplication(HttpServletRequest request) {
        String contextPath = getContextPath(request);
        String requestUri = getRequestUri(request);
        if (StringUtils.startsWithIgnoreCase(requestUri, contextPath)) {
            // Normal case: URI contains context path.
            String path = requestUri.substring(contextPath.length());
            return (StringUtils.hasText(path) ? path : "/");
        } else {
            // Special case: rather unusual.
            return requestUri;
        }
    }


public static String getRequestUri(HttpServletRequest request) {
        String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);  //URL:"/xxxx/..;/admin/index"
        if (uri == null) {
            uri = request.getRequestURI();
        }
        return normalize(decodeAndCleanUriString(request, uri));
    }

此时的URL还是我们传入的原始URL: /xxxx/..;/admin/index

接着,程序会进入到decodeAndCleanUriString(), 得到:

private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
        uri = decodeRequestString(request, uri);
        int semicolonIndex = uri.indexOf(';');
        return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
    }

decodeAndCleanUriString 以 ;截断后面的请求,所以此时返回的就是 /xxxx/...然后程序调用normalize() 对decodeAndCleanUriString()处理得到的路径进行标准化处理. 标准话的处理包括:

  • 替换反斜线
  • 替换 ///
  • 替换 /.//
  • 替换 /..//

都是一些很常见的标准化方法.

private static String normalize(String path, boolean replaceBackSlash) {

        if (path == null)
            return null;

        // Create a place for the normalized path
        String normalized = path;

        if (replaceBackSlash && normalized.indexOf('\\') >= 0)
            normalized = normalized.replace('\\', '/');

        if (normalized.equals("/."))
            return "/";

        // Add a leading "/" if necessary
        if (!normalized.startsWith("/"))
            normalized = "/" + normalized;

        // Resolve occurrences of "//" in the normalized path
        while (true) {
            int index = normalized.indexOf("//");
            if (index < 0)
                break;
            normalized = normalized.substring(0, index) +
                    normalized.substring(index + 1);
        }

        // Resolve occurrences of "/./" in the normalized path
        while (true) {
            int index = normalized.indexOf("/./");
            if (index < 0)
                break;
            normalized = normalized.substring(0, index) +
                    normalized.substring(index + 2);
        }

        // Resolve occurrences of "/../" in the normalized path
        while (true) {
            int index = normalized.indexOf("/../");
            if (index < 0)
                break;
            if (index == 0)
                return (null);  // Trying to go outside our context
            int index2 = normalized.lastIndexOf('/', index - 1);
            normalized = normalized.substring(0, index2) +
                    normalized.substring(index + 3);
        }

        // Return the normalized path that we have completed
        return (normalized);

    }

经过getPathWithinApplication()函数的处理,最终shiro 需要校验的URL 就是 /xxxx/... 最终会进入到 org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver 中的 getChain()方法会URL校验. 关键的校验方法如下:

由于 /xxxx/.. 并不会匹配到 /admin/**, 所以shiro权限校验就会通过.

最终我们的原始请求 /xxxx/..;/admin/index 就会进入到 springboot中. springboot对于每一个进入的request请求也会有自己的处理方式,找到自己所对应的mapping. 具体的匹配方式是在:org.springframework.web.util.UrlPathHelper 中的 getPathWithinServletMapping()

getPathWithinServletMapping() 在一般情况下返回的就是 servletPath, 所以本次中返回的就是 /admin/index.最终到了/admin/index 对应的requestMapping, 如此就成功地访问了后台请求.

最后,我们来数理一下整个请求过程:

  • 1.客户端请求URL: /xxxx/..;/admin/index
  • 2.shrio 内部处理得到校验URL为 /xxxx/..,校验通过。
  • 3.springboot 处理 /xxxx/..;/admin/index , 最终请求 /admin/index, 成功访问了后台请求。

总结

总的来说,这个漏洞还是比较简单的,虽然大部分shior都存在这个漏洞,但是实用性不大。


我从未觉得繁琐,说浪漫些,我很爱你。