前言

无。


环境搭建

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

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.9.0</version>
</dependency>

漏洞分析

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

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

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();
}

简单测试一下:

@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函数会调用该函数:

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:

public class MyFilter extends PathMatchingFilter {
    public MyFilter() {
        this.pathMatcher = new RegExPatternMatcher();
    }
}

然后添加到shiro里面:

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生成器:

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对象:

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

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

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

public PathMatchingFilterChainResolver() {
    this.pathMatcher = new AntPathMatcher();
    this.filterChainManager = new DefaultFilterChainManager();
}

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

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

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

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用于鉴权:

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:

@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路由的访问:

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

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

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

// 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函数:

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

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

漏洞利用

如上。

后记

利用范围好像有点小。


参考

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