前言

下一个Shiro认证绕过漏洞。


环境搭建

由于1.5.0版本的修复补丁考虑不全面导致绕过,最后在1.5.2版本完成修复。

shiro 1.4.2

测试用的代码跟上一个一样,不用改动。

不过上个版本的.绕过在这个版本是行不通的,因为此时的getPathWithinApplication函数获取到的路径没有经过标注化处理,其中会包含.这个字符。当然空格绕过还是行得通的。

此时的绕过方法为末尾加个/,即/admin/admin/:

此时在doMatch函数中,path被匹配完后会进入这个判断:

if (pattIdxStart > pattIdxEnd) {
    return (pattern.endsWith(this.pathSeparator) ?
            path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator));
}

因为path以/结尾,所以最后返回的就是false,就不会将认证所用的FormAuthenticationFilter加入要执行的filter链中,也就绕过了认证。

shiro 1.5.0

修复方式,在1.5.0版本下,getChain函数中加了个去掉末尾/的操作:

String requestURI = getPathWithinApplication(request);

// in spring web, the requestURI "/resource/menus" ---- "resource/menus/" bose can access the resource
// but the pathPattern match "/resource/menus" can not match "resource/menus/"
// user can use requestURI + "/" to simply bypassed chain filter, to bypassed shiro protect
if(requestURI != null && requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
    requestURI = requestURI.substring(0, requestURI.length() - 1);
}

shiro 1.5.1

认证流程与1.5.0版本没有变化,主要问题出在shiro和spring对于分号;的处理不同:

shiro

shiro通过getPathWithinApplication函数获取路径:

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);
    if (uri == null) {
        uri = request.getRequestURI();
    }
    return normalize(decodeAndCleanUriString(request, uri));
}

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函数中会将路径根据分号;截断,将分号;后面的字符都丢掉,获取到的路径就是/admin,自然也就匹配不上,绕过了认证。

spring

查询访问路径的代码在UrlPathHelper类的resolveAndCacheLookupPath函数中,而关键代码则在decodeAndCleanUriString函数:

private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
    uri = removeSemicolonContent(uri);
    uri = decodeRequestString(request, uri);
    uri = getSanitizedPath(uri);
    return uri;
}

decodeRequestString用于解码,getSanitizedPath用于去除重复的/,而removeSemicolonContent函数看起来用于删除分号;:

public String removeSemicolonContent(String requestUri) {
    return (this.removeSemicolonContent ?
            removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri));
}

private static String removeSemicolonContentInternal(String requestUri) {
    int semicolonIndex = requestUri.indexOf(';');
    if (semicolonIndex == -1) {
        return requestUri;
    }
    StringBuilder sb = new StringBuilder(requestUri);
    while (semicolonIndex != -1) {
        int slashIndex = sb.indexOf("/", semicolonIndex + 1);
        if (slashIndex == -1) {
            return sb.substring(0, semicolonIndex);
        }
        sb.delete(semicolonIndex, slashIndex);
        semicolonIndex = sb.indexOf(";", semicolonIndex);
    }
    return sb.toString();
}

简单来说就是把分号;到/之间的字符都吃了,所以/admin;n/admin也是可以绕过的。

shiro 1.5.2

getRequestUri函数做了修改:

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

修改了获取uri的方式,由数个函数的返回值拼凑起来,而getServletPath函数的返回值来自tomcat部分的mappingData,同样在CoyoteAdapter类的postParseRequest函数中处理得到,关键函数为parsePathParameters:

int start = uriBC.getStart();
int end = uriBC.getEnd();

int pathParamStart = semicolon + 1;
int pathParamEnd = ByteChunk.findBytes(uriBC.getBuffer(),
                                       start + pathParamStart, end,
                                       new byte[] {';', '/'});

String pv = null;

if (pathParamEnd >= 0) {
    if (charset != null) {
        pv = new String(uriBC.getBuffer(), start + pathParamStart,
                        pathParamEnd - pathParamStart, charset);
    }
    // Extract path param from decoded request URI
    byte[] buf = uriBC.getBuffer();
    for (int i = 0; i < end - start - pathParamEnd; i++) {
        buf[start + semicolon + i]
            = buf[start + i + pathParamEnd];
    }
    uriBC.setBytes(buf, start,
                   end - start - pathParamEnd + semicolon);
}

简单来说就是删掉了分号;和/之间的字符,如果没有/就把分号;后面的全删了。


参考文章

https://www.freebuf.com/vuls/249112.html


Web Shiro

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

CVE-2020-11989 Shiro认证绕过
CVE-2020-17523 Shiro认证绕过