前言
下一个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);
}
简单来说就是删掉了分号;和/之间的字符,如果没有/就把分号;后面的全删了。
参考文章
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!