CVE-2022-32532 Shiro认证绕过

前言

无。


环境搭建

按照漏洞通告,漏洞影响版本1.9.1之前的shiro,于是配置1.9.0版本的shiro依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.9.0</version>
</dependency>

漏洞分析

根据漏洞通告,问题就是RegExPatternMatcher类处理’.’这个正则字符时可能导致认证绕过,根据参考文章所说,也就是常常看到的不加修饰符情况下’.’不会匹配的问题。

RegExPatternMatcher类是使用Java内部提供的Pattern类完成的正则表达式解析工作:

1
2
3
4
5
6
7
8
public boolean matches(String pattern, String source) {
if (pattern == null) {
throw new IllegalArgumentException("pattern argument cannot be null.");
}
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(source);
return m.matches();
}

简单测试一下:

1
2
3
4
5
6
@RequestMapping("/test")
public String test() {
RegExPatternMatcher regExPatternMatcher = new RegExPatternMatcher();
System.out.println(regExPatternMatcher.matches(".*", "Twings\n"));
return "index";
}

可以看到此时RegExPatternMatcher无法匹配换行符,导致匹配失败,可以通过向URI写入换行符的方式绕过路径匹配。

然后问题就来了,RegExPatternMatcher并不是默认的路径匹配器,为了使用它我们还需要自定义一个东西将它配置进去。

搜索一下PatternMatcher.matches()函数会被调用的地方:

PathMatchingFilterChainResolver类的pathMatcher不好改也一般都不会改,可以看到另一个调用点是PathMatchingFilter类的pathsMatch函数会调用该函数:

1
2
3
4
5
protected boolean pathsMatch(String pattern, String path) {
boolean matches = pathMatcher.matches(pattern, path);
log.trace("Pattern [{}] matches path [{}] => [{}]", pattern, path, matches);
return matches;
}

其调用来自用于处理URI中非法字符的子类InvalidRequestFilter:

那么最容易想到的办法就是写一个继承了PathMatchingFilter的Filter,然后设置进shiro里面去。

先试一试,写一个自定义Filter:

1
2
3
4
5
public class MyFilter extends PathMatchingFilter {
public MyFilter() {
this.pathMatcher = new RegExPatternMatcher();
}
}

然后添加到shiro里面:

1
2
3
4
5
6
7
8
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(new MyRealm());
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
HashMap<String, Filter> filters = new HashMap();
filters.put("myFilter", new MyFilter());
shiroFilterFactoryBean.setFilters(filters);
map.put("/admin/.*", "myFilter");

没触发,看看是什么原因。

原因在于Tomcat要通过ApplicationFilterConfig获取要调用的filter,而在一般情况下,shiro配置的是一个DefaultWebSecurityManager的类中类SpringShiroFilter对象,而该类在实例化时会配置一个FilterChain生成器:

1
2
3
4
5
6
7
8
9
10
11
protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
super();
if (webSecurityManager == null) {
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
}
setSecurityManager(webSecurityManager);

if (resolver != null) {
setFilterChainResolver(resolver);
}
}

往上追溯可以看到这是一个PathMatchingFilterChainResolver对象:

1
2
3
4
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);

return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);

这个FilterChain生成器在进行路径匹配时使用的是AntPathMatcher这个路径匹配器:

1
2
3
4
public PathMatchingFilterChainResolver() {
this.pathMatcher = new AntPathMatcher();
this.filterChainManager = new DefaultFilterChainManager();
}

看起来这个漏洞不是这么玩的。

换句话说就是要触发这个漏洞,我们要做的不是自定义一个使用RegExPatternMatcher这个路径匹配器的Filter然后配置到shiro里面,而是将SpringShiroFilter-filterChainResolver-pathMatcher重新配置为RegExPatternMatcher,也就是通过上文中PatternMatcher.matches()的另一个触发点。

为了实现这个目标,按照正常项目需求应该就是自定义一个ShiroFilterFactoryBean,直接把ShiroFilterFactoryBean的大部分代码复制过去就行:

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
31
32
33
34
35
36
37
38
public class MyShiroFilterFactoryBean extends ShiroFilterFactoryBean {
@Override
protected AbstractShiroFilter createInstance() {
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}

if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}

FilterChainManager manager = createFilterChainManager();

PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setPathMatcher(new RegExPatternMatcher());
chainResolver.setFilterChainManager(manager);

return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}

private static final class SpringShiroFilter extends AbstractShiroFilter {

protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
super();
if (webSecurityManager == null) {
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
}
setSecurityManager(webSecurityManager);

if (resolver != null) {
setFilterChainResolver(resolver);
}
}
}
}

再把shiro config还原一下。

然而问题又来了,此时应用环境还是没有配置完全,尽管给需要认证的路由配置了authc的权限要求,但是由于shiro内部的认证用的是filter对象FormAuthenticationFilter,它父类PathMatchingFilter的preHandle函数还要进行一次pathsMatch,里面使用的仍然是AntPathMatcher。

所以还得写一个自定义filter用于鉴权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyFilter extends AccessControlFilter {
public MyFilter() {
this.pathMatcher = new RegExPatternMatcher();
}

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
saveRequestAndRedirectToLogin(request, response);
return false;
}
}

再修改一下Config:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class MyShiroConfig {
@Bean
public ShiroFilterFactoryBean myShiroFilterFactoryBean() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(new MyRealm());
MyShiroFilterFactoryBean myShiroFilterFactoryBean = new MyShiroFilterFactoryBean();
myShiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
HashMap<String, Filter> filters = new HashMap<>();
filters.put("myFilter", new MyFilter());
myShiroFilterFactoryBean.setFilters(filters);
Map<String, String> map = new HashMap<>();
map.put("/shiro", "authc");
map.put("/logout", "user");
map.put("/user", "user");
map.put("/admin/.*", "myFilter");
myShiroFilterFactoryBean.setLoginUrl("/index");
myShiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return myShiroFilterFactoryBean;
}
}

此时能正确拦截对/admin路由的访问:

加入换行符,发现出现了路由匹配问题:

1
2
3
java.util.regex.PatternSyntaxException: Dangling meta character '*' near index 2
/**
^

追溯一下,可以看到ShiroFilterFactoryBean在调用createFilterChainManager函数创建FilterChainManager时会同时创建一个默认路由:

1
2
// create the default chain, to match anything the path matching would have missed
manager.createDefaultChain("/**"); // TODO this assumes ANT path matching, which might be OK here

然而这个写法是给AntPathMatcher用的,在RegExPatternMatcher里会发生错误,改一下自定义ShiroFilterFactoryBean的createInstance函数:

1
2
3
FilterChainManager manager = new DefaultFilterChainManager();
manager.addFilter("myFilter", new MyFilter());
manager.addToChain("/admin/.*", "myFilter");

再删掉Config中的路由配置,可以看到换行符可以成功绕过认证:

漏洞利用

如上。

后记

利用范围好像有点小。


参考

【技术干货】 CVE-2022-32532 Apache Shiro RegExPatternMatcher 认证绕过漏洞


CVE-2022-32532 Shiro认证绕过
http://yoursite.com/2022/11/24/CVE-2022-32532-Shiro认证绕过/
作者
Aluvion
发布于
2022年11月24日
许可协议