在Spring-Security
的 CVE-2022-22978 漏洞爆出后,陈师傅和我是killer师傅迅速在星球《漏洞百出》给出了实际的案例:在SpringMVC
下的利用场景以及拓展思路。当天阅读完发现这并不是Spring Security
特有的漏洞,而是一种半通用的思路,连夜分析了Apache Shiro
和Spring MVC
以及其他可能的Java
组件,甚至分析了Django
和Ruby on Rails
等其他语言框架,在凌晨发送了两篇漏洞报告,分别到Shiro
和Spring
的安全团队。对于SpringMVC
中的问题我将在后文中解释,他们认为这并不是漏洞,仅是一个issue
或者说改进功能
两句话评价CVE-2022-32532
漏洞:这是一个鸡肋洞,需要罕见的配置下才可以绕过。假设能够绕过shiro
的身份验证,在后端程序中大概率还会有其他的验证(以前的shiro
绕过都有类似的问题)
无论是CVE-2022-22978
还是CVE-2022-32532
本质都是以下的内容
在Java
中的正则默认情况下.
并不包含\r
和\n
字符,因此某些情况下正则规则可以被绕过
String regex = "a.*b"; Pattern pattern = Pattern.compile(regex); boolean flag1 = pattern.matcher("aaabbb").matches(); // true System.out.println(flag1); boolean flag2 = pattern.matcher("aa\nbb").matches(); // false System.out.println(flag2);
虽然说编写正则是开发者的责任,如果是完善的正则表达式则不会出现这类漏洞。但在开发者的意识中:如果配置了/permit/.*
路径规则,他的目标应该是拦截所有/permit/
下的请求,如果出现了意料之外的问题,可以认为是一种安全风险。从框架角度来说,有必要针对这种问题改善部分代码,目标是在通常的意识中不会出现意外的情况。针对于这种问题的修复其实很简单,加入一个flag
即可
String regex = "a.*b"; // add DOTALL flag Pattern pattern = Pattern.compile(regex,Pattern.DOTALL); boolean flag1 = pattern.matcher("aaabbb").matches(); // true System.out.println(flag1); boolean flag2 = pattern.matcher("aa\nbb").matches(); // true System.out.println(flag2);
简单阅读Shiro
源码后发现这样一个类:RegExPatternMatcher
参考上文的原理,一眼即可看出可能存在安全风险
public class RegExPatternMatcher implements PatternMatcher { // ... public boolean matches(String pattern, String source) { if (pattern == null) { throw new IllegalArgumentException("pattern argument cannot be null."); } // no DOTALL flag Pattern p = Pattern.compile(pattern); Matcher m = p.matcher(source); return m.matches(); } }
但不能仅因为一个类而确定安全漏洞,至少搭建出一个可用的漏洞环境,才有必要发送漏洞报告
在shiro
中默认配置的Matcher
是AntPathMatcher
类,用于路径的匹配。而RegExPatternMatcher
仅仅是shiro
向开发者提供的另一个Matcher
实现,在整个shiro
项目中都没有出现,需要用户自行配置
使用shiro
的过程中通常会有以下的配置
@Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> map = new HashMap<>(); // 登出 map.put("/logout", "logout"); // 所有路径都需要认证 map.put("/**", "authc"); // 登录 shiroFilterFactoryBean.setLoginUrl("/login"); // 首页 shiroFilterFactoryBean.setSuccessUrl("/index"); // 错误页面,认证不通过跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/error"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; }
其中的map
记录了路径匹配的规则,跟入ShiroFilterFactoryBean
分析,分析SecurityManager
如何处理以上的配置
protected AbstractShiroFilter createInstance() throws Exception { // 跟入分析 FilterChainManager manager = createFilterChainManager(); PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver(); chainResolver.setFilterChainManager(manager); return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver); }
在createFilterChainManager
方法处理完配置之后得到FilterChainManager
对象。并配置一个Resolver
类,该类名称很长,实际上可以理解为一个工具类,负责对路径进行匹配,根据规则处理每一个请求,判断该请求路径是否匹配到规则,将在后文分析。首先来看createFilterChainManager
方法
protected FilterChainManager createFilterChainManager() { DefaultFilterChainManager manager = new DefaultFilterChainManager(); // 默认的一系列filter // 例如authc和anon等规则对应的filter Map<String, Filter> defaultFilters = manager.getFilters(); // 向默认的filter添加规则集(登录url等信息) for (Filter filter : defaultFilters.values()) { applyGlobalPropertiesIfNecessary(filter); } // 用户是否自定义了其他的filter Map<String, Filter> filters = getFilters(); if (!CollectionUtils.isEmpty(filters)) { // 逐个处理用户自定义的filter for (Map.Entry<String, Filter> entry : filters.entrySet()) { String name = entry.getKey(); Filter filter = entry.getValue(); applyGlobalPropertiesIfNecessary(filter); if (filter instanceof Nameable) { ((Nameable) filter).setName(name); } manager.addFilter(name, filter, false); } } // 设置全局的filter // 默认下只有InvalidRequestFilter manager.setGlobalFilters(this.globalFilters); // 这里获得了我们配置的规则集 // 例如map.put("/**", "authc"); Map<String, String> chains = getFilterChainDefinitionMap(); if (!CollectionUtils.isEmpty(chains)) { for (Map.Entry<String, String> entry : chains.entrySet()) { String url = entry.getKey(); String chainDefinition = entry.getValue(); // 解析规则添加到filter链中 manager.createChain(url, chainDefinition); } } // 添加最后的链用于处理所有规则遗漏的部分 manager.createDefaultChain("/**"); return manager; }
配置中的anon
等字符串映射到对应的Filter
中,如果我们想要自定义filter
首先应该看懂这些默认的filter
代码
public enum DefaultFilter { anon(AnonymousFilter.class), authc(FormAuthenticationFilter.class), // ... }
FormAuthenticationFilter
的父类是AuthenticatingFilter
类,它的继承实现关系如图
在服务端收到请求并传递到Servlet
或Controller
之前首先交给Filter
处理
OncePerRequestFilter
基类用于防止多次执行Filter
并保证一次请求只会走一次拦截器链
AdviceFilter
提供了AOP风格的支持,类似于SpringMVC
中的Interceptor
,其中定义了前置和后置增强处理的方法
PathMatchingFilter
基于AOP提供了请求路径匹配功能及拦截器参数解析的功能
AccessControlFilter
类是更偏向于上层的类,提供了访问控制的基础功能,比如是否允许访问或当访问拒绝时如何处理等
回到FormAuthenticationFilter
类分析
public class FormAuthenticationFilter extends AuthenticatingFilter { public FormAuthenticationFilter() { setLoginUrl(DEFAULT_LOGIN_URL); } // 认证失败后的处理过程 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { // 是否是登录请求在 父类的父类 AccessControlFilter中实现 if (isLoginRequest(request, response)) { // 判断是否是POST的登录请求 if (isLoginSubmission(request, response)) { // 如果是登录url则执行登录 // 该方法在父类AuthenticatingFilter中实现 return executeLogin(request, response); } else { // 不是具体的登录请求但URL符合登录条件 // 也就是说这可能是一个普通的GET /login // 返回true接下来返回到按照普通GET /login处理 return true; } } else { // 不是登录请求则重定向到的登录页面 saveRequestAndRedirectToLogin(request, response); return false; } } } // 是否登录请求 protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) { return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD); }
进入AccessControlFilter
分析如何进行路径匹配
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) { // 进入父类PathMatchingFilter的pathsMatch(String,ServletRequest)方法 return pathsMatch(getLoginUrl(), request); }
可以发现分析和处理URL的类是PathMatchingFilter
类
public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor { // 默认情况下的pathMatcher是AntPathMatcher protected PatternMatcher pathMatcher = new AntPathMatcher(); // 记录了以及配置的规则但value一般是null(如/**->null) protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>(); // 核心方法:路径匹配(成功返回true) protected boolean pathsMatch(String path, ServletRequest request) { String requestURI = getPathWithinApplication(request); boolean match = pathsMatch(path, requestURI); // ... return match; } protected boolean pathsMatch(String pattern, String path) { // 找到了matcher.matches调用 boolean matches = pathMatcher.matches(pattern, path); return matches; } // 前置处理 protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { if (this.appliedPaths == null || this.appliedPaths.isEmpty()) { // 允许filter链继续执行 return true; } for (String path : this.appliedPaths.keySet()) { if (pathsMatch(path, request)) { log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path); Object config = this.appliedPaths.get(path); return isFilterChainContinued(request, response, path, config); } } // 允许filter链继续执行 return true; } }
在PathMatchingFilter
类中找到了matcher.matches
方法调用,并且发现了默认情况下使用了AntPathMatcher
类而不是正则的Matcher
类
其实以上这么多的分析,目的是研究自定义Filter
如何使用RegExPatternMatcher
类
在阅读了一些shiro
相关的开源项目后,发现他们总是继承AccessControlFilter
做一些自定义。因为该类可以使用父类的pathsMatch
方法进行匹配,且该类提供了几个实用的方法,在做web
开发中很方便上手:
isAccessAllowed
方法:用户自定义怎样的情况下认证成功onAccessDenied
方法:自定义认证失败后需要做什么基于以上的原理,结合真实的场景,会出现以下这样的自定义Filter
Token
是否匹配Token
或者Token
头错误则认为认证失败public class MyFilter extends AccessControlFilter { public MyFilter(){ super(); // 注意自定义父类的pathMatcher属性为RegExPatternMatcher this.pathMatcher = new RegExPatternMatcher(); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { String token = ((HttpServletRequest)request).getHeader("Token"); // 实际上应该从数据库或者其他地方查询验证 // 这里仅简单地验证是否为4ra1n即可 return token != null && token.equals("4ra1n"); } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) { System.out.println("deny -> "+((HttpServletRequest)request).getRequestURI()); try { response.getWriter().println("access denied"); } catch (IOException e) { e.printStackTrace(); } return false; } }
虽然ShiroFilterFactoryBean
提供了设置自定义Filter
的方法,但该方法仅适用于使用Ant
类型的Mathcer
如果想要使用RegExPatternMatcher
还有坑
public void setFilters(Map<String, Filter> filters) { this.filters = filters; }
可能是shiro
设计方面的缺陷,在PathMatchingFilterChainResolver
的两个构造方法中都使用了AntPathMatcher
类,不支持在构造的时候设置Mathcer
只提供了setMathcer
这样的方法,在ShiroFilterFactoryBean
的构造方法中同样不支持自定义Matcher
,都默认使用了Ant
的Matcher
类
public PathMatchingFilterChainResolver() { this.pathMatcher = new AntPathMatcher(); this.filterChainManager = new DefaultFilterChainManager(); } public PathMatchingFilterChainResolver(FilterConfig filterConfig) { this.pathMatcher = new AntPathMatcher(); this.filterChainManager = new DefaultFilterChainManager(filterConfig); }
为了解决这个坑,自定义MyShiroFilterFactoryBean
继承自ShiroFilterFactoryBean
类,添加自定义的MyFilter
并设置匹配规则为/permit/.*
字符串,表示需要拦截/permit/
下所有的路径,设置到Filter
链管理器DefaultFilterChainManager
中。并在最后指定PathMatchingFilterChainResolver
的pathMatcher
属性为RegExPatternMatcher
否则会使用默认的Ant
类型Matcher
我在github
的一些开源shiro
程序中看到自定义PathMatchingFilterChainResolver
类的例子,也是另一种设置pathMatcher
属性的方式
@Override protected AbstractShiroFilter createInstance() { SecurityManager securityManager = this.getSecurityManager(); FilterChainManager manager = new DefaultFilterChainManager(); manager.addFilter("myFilter",new MyFilter()); // my filter manager.addToChain("/permit/.*", "myFilter"); // todo: add other filters PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver(); chainResolver.setFilterChainManager(manager); // set RegExPatternMatcher chainResolver.setPathMatcher(new RegExPatternMatcher()); return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver); }
给出一段来自真实开源项目的代码,他自定的Filter
继承自AuthorizationFilter
类,并且自定义了PathMatchingFilterChainResolver
类
public MyPermissionsAuthorizationFilter(boolean regexExp) { super(); this.regexExpMatcher = regexExp; if (regexExp) { pathMatcher = new RegExPatternMatcher(); } } protected boolean pathsMatch(String pattern, ServletRequest request) { String requestURI = getPathWithinApplication(request); if (request instanceof HttpServletRequest) { String queryString = ((HttpServletRequest) request).getQueryString(); if (regexExpMatcher && !(queryString == null || queryString.length() == 0)) requestURI += ("?" + queryString); } String regex = pattern; if (regexExpMatcher) regex = MyPathMatchingFilterChainResolver.replacePattern(pattern); return pathsMatch(regex, requestURI); }
评价该漏洞:比较鸡肋的洞
漏洞的利用条件:目标配置了RegExPatternMatcher
情况下且正则规则中包含了“.”则存在漏洞
漏洞测试环境:https://github.com/4ra1n/CVE-2022-32532
漏洞的利用场景如下:
/permit/{value}
这样从路径取参数的路由/permit/*
这样的通配路由@RestController public class DemoController { @RequestMapping(path = "/permit/{value}") public String permit(@PathVariable String value) { return "success"; } @RequestMapping(path = "/permit/*") public String permit() { return "success"; } }
在JavaWeb
项目中并不完全使用Shiro
或Spring-Security
进行权限管理,在一些老站和入门程序中会使用SpringMVC
的Interceptor
功能
例如这里自定义一个Interceptor
继承子HandlerInterceptor
类,如果认证失败则不会到达Controller
@Component public class PermissionInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // authorization System.out.println(request.getRequestURI()); return false; } }
配置该Interceptor
到SpringMVC
中,设置路径为/permit/.*
期望拦截所有/permit/
下的请求
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new PermissionInterceptor()).addPathPatterns("/permit/{username:.*}"); } }
定义对应的Controller
@RequestMapping("/permit/*") @ResponseBody public String test(HttpServletRequest request) throws Exception { return "ok"; }
我将该问题报告到Spring
框架后回复如下
从邮件回复中可以看出SpringMVC
团队认为不应该用Interceptor
来保证安全性,应该交给Spring Security
来负责安全
他们不打算发布CVE但认可了该漏洞或者说是错误,并打算在下一次新版本发布中修复和向我致谢