在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();// trueSystem.out.println(flag1);boolean flag2 = pattern.matcher("aa\nbb").matches();// falseSystem.out.println(flag2);虽然说编写正则是开发者的责任,如果是完善的正则表达式则不会出现这类漏洞。但在开发者的意识中:如果配置了/permit/.*路径规则,他的目标应该是拦截所有/permit/下的请求,如果出现了意料之外的问题,可以认为是一种安全风险。从框架角度来说,有必要针对这种问题改善部分代码,目标是在通常的意识中不会出现意外的情况。针对于这种问题的修复其实很简单,加入一个flag即可
String regex = "a.*b";// add DOTALL flagPattern pattern = Pattern.compile(regex,Pattern.DOTALL);boolean flag1 = pattern.matcher("aaabbb").matches();// trueSystem.out.println(flag1);boolean flag2 = pattern.matcher("aa\nbb").matches();// trueSystem.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的过程中通常会有以下的配置
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();    }
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {        String token = ((HttpServletRequest)request).getHeader("Token");        // 实际上应该从数据库或者其他地方查询验证        // 这里仅简单地验证是否为4ra1n即可        return token != null && token.equals("4ra1n");    }
        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属性的方式
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情况下且正则规则中包含了“.”则存在漏洞
漏洞的利用场景如下:
/permit/{value}这样从路径取参数的路由/permit/*这样的通配路由public class DemoController {    (path = "/permit/{value}")    public String permit( String value) {        return "success";    }     (path = "/permit/*")    public String permit() {        return "success";    }}在JavaWeb项目中并不完全使用Shiro或Spring-Security进行权限管理,在一些老站和入门程序中会使用SpringMVC的Interceptor功能
例如这里自定义一个Interceptor继承子HandlerInterceptor类,如果认证失败则不会到达Controller
public class PermissionInterceptor implements HandlerInterceptor {        public boolean preHandle(HttpServletRequest request,                             HttpServletResponse response,                             Object handler) throws Exception {        // authorization        System.out.println(request.getRequestURI());        return false;    }}配置该Interceptor到SpringMVC中,设置路径为/permit/.*期望拦截所有/permit/下的请求
public class WebMvcConfig implements WebMvcConfigurer {        public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(new PermissionInterceptor()).addPathPatterns("/permit/{username:.*}");    }}定义对应的Controller
("/permit/*")public String test(HttpServletRequest request) throws Exception {    return "ok";}我将该问题报告到Spring框架后回复如下

从邮件回复中可以看出SpringMVC团队认为不应该用Interceptor来保证安全性,应该交给Spring Security来负责安全
他们不打算发布CVE但认可了该漏洞或者说是错误,并打算在下一次新版本发布中修复和向我致谢