CVE-2020-1957分析

简介

Apache Shiro是一个安全框架,在Java开发中使用非常广,可以很方便地做权限管理。一个权限绕过漏洞,危害不是很大,因为一般情况下绕过Shiro还有具体的操作权限验证,很少发生绕过Shiro获取很大权限这种事。最有名的Shiro反序列化漏洞打算后续分析

漏洞复现

下载官方示例:https://github.com/lenve/javaboy-code-samples

使用其中的shiro/shiro-basic项目,修改org\javaboy\shirobasic\ShiroConfig.java代码,修改配置,设置登录url为/login,设置成功后跳转的url是/index,设置登录失败的url为/unauthorizedurl,对于/admin下的所有路径都进行权限验证

@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean() {
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    bean.setSecurityManager(securityManager());
    bean.setLoginUrl("/login");
    bean.setSuccessUrl("/index");
    bean.setUnauthorizedUrl("/unauthorizedurl");
    Map<String, String> map = new LinkedHashMap<>();
    map.put("/admin/**", "authc");
    bean.setFilterChainDefinitionMap(map);
    return bean;
}

添加一个路由,在org\javaboy\shirobasic\LoginController.java中增加如下代码

@RequestMapping("/index")
public String index(){
    return "This is index page";
}

@RequestMapping("/admin/index")
public String test() {
    return "This is admin index page";
}

启动项目,访问/index,没有被拦截

访问/admin/index,不允许访问,跳到登录页

输入http://localhost:8080/xxx/..;/admin/index成功绕过

漏洞分析

从大体上理解,其实让Shiro和SpringBoot获取到的URL不同,就可以做到绕过

从shiro获取uri的入口分析:org\apache\shiro\web\util\WebUtils.java,可以看到,通过getRequestUri函数获得的uri并不是真正的

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

跟入getRequestUri函数,可以看到在return normalize(decodeAndCleanUriString(request, uri));之前,uri还是正确的

public static String getRequestUri(HttpServletRequest request) {
    String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);
    if (uri == null) {
        uri = request.getRequestURI();
    }
    return normalize(decodeAndCleanUriString(request, uri));
}

重点来看normalizedecodeAndCleanUriString方法。不难看出, decodeAndCleanUriString方法进行了字符串截断,最后得到的uri是/xxx/..

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

其中normalize方法并没有什么特殊的地方,一个标准库,替换反斜杠,去除双斜杠,处理/..//./等特殊情况,而获取到的uri会在org\apache\shiro\web\filter\mgt\PathMatchingFilterChainResolver.java类的getChain方法做校验,而/xxx/..并不会匹配到/admin/**

public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
    FilterChainManager filterChainManager = getFilterChainManager();
    if (!filterChainManager.hasChains()) {
        return null;
    }

    String requestURI = getPathWithinApplication(request);

    //the 'chain names' in this implementation are actually path patterns defined by the user.  We just use them
    //as the chain name for the FilterChainManager's requirements
    for (String pathPattern : filterChainManager.getChainNames()) {

        // If the path does match, then pass on to the subclass implementation for specific checks:
        if (pathMatches(pathPattern, requestURI)) {
            if (log.isTraceEnabled()) {
                log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "].  " +
                        "Utilizing corresponding filter chain...");
            }
            return filterChainManager.proxy(originalChain, pathPattern);
        }
    }

    return null;
}

到这里就绕过了Shiro,但还要让SpringBoot解析成对应的URL漏洞才会生效。找到SpringBoot解析URL的类org\springframework\web\util\UrlPathHelper.javagetPathWithinServletMapping方法,断点调试后发现返回的是/admin/index,成功触发漏洞。HttpServletRequest.getServletPath()是标准方法,并不存在漏洞,所以本质上还是shiro的校验问题

补丁分析

官方补丁:https://github.com/apache/shiro/commit/3708d7907016bf2fa12691dff6ff0def1249b8ce

以上文的payload/xxx/...;/admin/index,这里uri会变成//admin/index

  • request.getContextPath()--->""
  • request.getServletPath()--->"/admin/index"
  • request.getPathInfo()--->""
public static String getRequestUri(HttpServletRequest request) {
    String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);
    if (uri == null) {
        uri = request.getRequestURI();
        uri = valueOrEmpty(request.getContextPath()) + "/" +
                valueOrEmpty(request.getServletPath()) +
                valueOrEmpty(request.getPathInfo());
    }
    return normalize(decodeAndCleanUriString(request, uri));
}