前言 无。
环境搭建 按照漏洞通告 ,漏洞影响版本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 manager.createDefaultChain("/**" );
然而这个写法是给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 认证绕过漏洞