文章首发于TOOLS
本周检测到Apache shiro安全更新,修复安全漏洞Apache Shiro权限绕过漏洞(CVE-2020-1957)。自信分析缺陷代码及其commit后发现是url规则Filter处理时产生的漏洞,该漏洞从第一次issue、commit到第二次issue、commit(2019-03-25->2020-02-13)较长的时间才修复完成,漏洞非常简单且影响面有限,本文以CVE-2020-1957结合shiro框架Filter做简单分析。

本地环境

shiroConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/admin");
bean.setSuccessUrl("/index");
Map<String, String> map = new LinkedHashMap<>();
map.put("/login", "anon");
map.put("/health", "authc");
// map.put("/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}

使用LinkedHashMap保存url、filter对应关系及url匹配、解析顺序,通过bean.setFilterChainDefinitionMap(map);bean注入设置对应配置。

Shiro Filter

所有request访问时都会先经过shiro做认证、权限过滤,在AbstractShiroFilter.executeChain()方法根据request解析的path找到shiroFilterFactoryBean中对应的过滤规则,执行对应的Filter,最后执行业务逻辑并response。

结合源码分析request经过shiroFilter的流转处理过程

doFilter

request请求到达spring后执行对应类的doFilter,查找/创建的shiroFilter对象,调用org/apache/shiro/web/servlet/OncePerRequestFilter.class的doFilter方法,通过request是否已有对应过滤Filter属性等通过this.doFilterInternal(request, response, filterChain);实际调用执行/org/apache/shiro/web/servlet/AbstractShiroFilter.class的doFilter方法,该方法主要是subject创建、执行和清理shiro需要过滤的request逻辑的实现,核心逻辑为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException {
Throwable t = null;

try {
......
subject.execute(new Callable() {
public Object call() throws Exception {
AbstractShiroFilter.this.updateSessionLastAccessTime(request, response);
AbstractShiroFilter.this.executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException var8) {
t = var8.getCause();
} catch (Throwable var9) {
t = var9;
}

......
}

调用executeChain方法,通过config找对应的执行方式然后执行getExecutionChain()方法FilterChain的返回值

1
2
3
4
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) throws IOException, ServletException {
FilterChain chain = this.getExecutionChain(request, response, origChain);
chain.doFilter(request, response);
}

跟进getExecutionChain(),核心逻辑及两个核心方法

1
2
3
4
5
6
7
8
9
10
11
12
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
FilterChainResolver resolver = this.getFilterChainResolver();
if (resolver == null) {
log.debug("No FilterChainResolver configured. Returning original FilterChain.");
return origChain;
} else {
FilterChain resolved = resolver.getChain(request, response, origChain);
......
return chain;
}
}
  • getFilterChainResolver()是在创建shiroFilter时传递的FilterChainResolver
  • getChain()方法是config中配置的request url的匹配实现-pathMatcher
    跟进/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.class的getChain方法,核心逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
......

Iterator var6 = filterChainManager.getChainNames().iterator();

String pathPattern;
do {
if (!var6.hasNext()) {
return null;
}

pathPattern = (String)var6.next();
if (pathPattern != null && pathPattern.endsWith("/")) {
pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
}
} while(!this.pathMatches(pathPattern, requestURI));
......
}

实际是一个for循环,匹配到url规则时返回对应的FilterChain给容器执行
主要匹配逻辑是在/org/apache/shiro/web/filter/PathMatchingFilter.class的pathsMatch()中实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
protected boolean pathsMatch(String path, ServletRequest request) {
String requestURI = this.getPathWithinApplication(request);
if (requestURI != null && !"/".equals(requestURI) && requestURI.endsWith("/")) {
requestURI = requestURI.substring(0, requestURI.length() - 1);
}

if (path != null && !"/".equals(path) && path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}

log.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, Encode.forHtml(requestURI));
return this.pathsMatch(path, requestURI);
}

CVE-2020-1957

看完shiro Filter的逻辑后,跟进分析下CVE-2020-1957漏洞,[CVE-2020-1957] Apache Shiro 1.5.2 released中描述是shiro框架配合Spring dynamic
controllers时存在权限绕过,如/health = authc,请求/health/可以绕过authc Filter过滤。

SHIRO-682

2019年3月25日shiro提交issue,当使用requestURI + “/“可以绕过shiro保护
2019年11月20日commit解决该issue

1
2
3
4
5
6
if (requestURI != null && requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
requestURI = requestURI.substring(0, requestURI.length() - 1);
}
if (path != null && path.endsWith(DEFAULT_PATH_SEPARATOR)) {
path = path.substring(0, path.length() - 1);
}

即在url匹配pathsMatch时判断requestURI是否已’/‘结尾,如果是就去除’/‘

SHIRO-742

第一次修复后对requestURI没有考虑root path的情况会导致报错Can not get the NamedFilterList when request uri is &quot;/&quot;.
2020年2月13 commit fix bug

1
2
if (requestURI != null && !DEFAULT_PATH_SEPARATOR.equals(requestURI)
&& requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
即考虑requestURI是’/‘的情况,不做去除’/‘处理