CVE-2020-1957 Shiro认证绕过

前言

下一个Shiro认证绕过漏洞。


环境搭建

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

shiro 1.4.2

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

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

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

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

1
2
3
4
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函数中加了个去掉末尾/的操作:

1
2
3
4
5
6
7
8
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函数获取路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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函数:

1
2
3
4
5
6
private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
uri = removeSemicolonContent(uri);
uri = decodeRequestString(request, uri);
uri = getSanitizedPath(uri);
return uri;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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函数做了修改:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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


CVE-2020-1957 Shiro认证绕过
http://yoursite.com/2021/11/09/CVE-2020-1957-Shiro认证绕过/
作者
Aluvion
发布于
2021年11月9日
许可协议