• Apache shiro RegExPatternMatcher 权限绕过漏洞 (CVE-2022-32532)


    漏洞描述

    2022年6月29日,Apache 官方披露 Apache Shiro (CVE-2022-32532)权限绕过漏洞。
    Apache Shiro中使用RegexRequestMatcher进行权限配置,且正则表达式中携带"."时,未经授权的远程攻击者可通过构造恶意数据包绕过身份认证,导致配置的权限验证失效。

    相关介绍

    Apache Shiro 是一个功能强大且易于使用的 Java 安全框架,它可以执行身份验证、授权、加密和会话管理,可以用于保护任何应用程序——从命令行应用程序、移动应用程序到最大的 web 和企业应用程序。

    影响版本

    安全版本:Apache Shiro = 1.9.1
    受影响版本:Apache Shiro < 1.9.1

    漏洞

    贴上我遇到的漏洞截图,如下图:
    在这里插入图片描述
    根据提示将相关包的版本升级至1.9.1,打包程序并部署。
    发现还是会扫描出该漏洞,依次升级至1.10.01.11.01.12.0,直到1.12.0版本该漏洞未出现了。

    本以为是简单的版本升级,结果发现登录请求里的createToken之类的方法每次都执行2次;

    跟踪代码

    • Shiro配置类ShiroConfig中的shiroFilterFactoryBean方法
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager)
        {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            // Shiro的核心安全接口,这个属性是必须的
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            // 身份认证失败,则跳转到登录页面的配置
            shiroFilterFactoryBean.setLoginUrl(loginUrl);
            // 权限认证失败,则跳转到指定页面
            shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
            // Shiro连接约束配置,即过滤链的定义
            LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
            // 对静态资源设置匿名访问
            ...........
            return shiroFilterFactoryBean;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 进一步追踪
    private void applyGlobalPropertiesIfNecessary(Filter filter) {
            this.applyLoginUrlIfNecessary(filter);
            this.applySuccessUrlIfNecessary(filter);
            this.applyUnauthorizedUrlIfNecessary(filter);
            if (filter instanceof OncePerRequestFilter) {
                ((OncePerRequestFilter)filter).setFilterOncePerRequest(this.filterConfiguration.isFilterOncePerRequest());
            }
    
        }
    
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            if (bean instanceof Filter) {
                log.debug("Found filter chain candidate filter '{}'", beanName);
                Filter filter = (Filter)bean;
                this.applyGlobalPropertiesIfNecessary(filter);
                this.getFilters().put(beanName, filter);
            } else {
                log.trace("Ignoring non-Filter bean '{}'", beanName);
            }
            return bean;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    从上面方法可以看出来postProcessBeforeInitialization方法在bean初始化的时候去执行, 将自定义的登录过滤器中的setFilterOncePerRequest设置为了ShiroFilterConfiguration实例中给定的值;
    其值默认是false,未启用OncePerRequestFilter的只执行一次机制

    OncePerRequestFilter 类的核心方法(1.9.0和1.12.0版本的区别)

    • Apache Shiro = 1.9.0时,OncePerRequestFilter类源码
    package org.apache.shiro.web.servlet;
    
    import java.io.IOException;
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public abstract class OncePerRequestFilter extends NameableFilter {
        private static final Logger log = LoggerFactory.getLogger(OncePerRequestFilter.class);
        public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";
        private boolean enabled = true;
    
        public OncePerRequestFilter() {}
        public boolean isEnabled() { return this.enabled; }
        public void setEnabled(boolean enabled) { this.enabled = enabled; }
    
        public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
            if (request.getAttribute(alreadyFilteredAttributeName) != null) {
                log.trace("Filter '{}' already executed.  Proceeding without invoking this filter.", this.getName());
                filterChain.doFilter(request, response);
            } else if (this.isEnabled(request, response) && !this.shouldNotFilter(request)) {
                log.trace("Filter '{}' not yet executed.  Executing now.", this.getName());
                request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
                try {
                    this.doFilterInternal(request, response, filterChain);
                } finally {
                    request.removeAttribute(alreadyFilteredAttributeName);
                }
            } else {
                log.debug("Filter '{}' is not enabled for the current request.  Proceeding without invoking this filter.", this.getName());
                filterChain.doFilter(request, response);
            }
        }
    
        protected boolean isEnabled(ServletRequest request, ServletResponse response) throws ServletException, IOException {
            return this.isEnabled();
        }
    
        protected String getAlreadyFilteredAttributeName() {
            String name = this.getName();
            if (name == null) {
                name = this.getClass().getName();
            }
            return name + ".FILTERED";
        }
    
        /** @deprecated */
        @Deprecated
        protected boolean shouldNotFilter(ServletRequest request) throws ServletException {
            return false;
        }
    
        protected abstract void doFilterInternal(ServletRequest var1, ServletResponse var2, FilterChain var3) throws ServletException, IOException;
    }
    
    • 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
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • Apache Shiro = 1.12.0时,OncePerRequestFilter 类源码
    package org.apache.shiro.web.servlet;
    
    import java.io.IOException;
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public abstract class OncePerRequestFilter extends NameableFilter {
        private static final Logger log = LoggerFactory.getLogger(OncePerRequestFilter.class);
        public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";
        private boolean enabled = true;
        private boolean filterOncePerRequest = false;
        
        public OncePerRequestFilter() {}
        public boolean isEnabled() { return this.enabled; }
        public void setEnabled(boolean enabled) { this.enabled = enabled; }
        public boolean isFilterOncePerRequest() { return this.filterOncePerRequest; }
        public void setFilterOncePerRequest(boolean filterOncePerRequest) {
            this.filterOncePerRequest = filterOncePerRequest;
        }
    
        public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
            if (request.getAttribute(alreadyFilteredAttributeName) != null && this.filterOncePerRequest) {
                log.trace("Filter '{}' already executed.  Proceeding without invoking this filter.", this.getName());
                filterChain.doFilter(request, response);
            } else if (this.isEnabled(request, response) && !this.shouldNotFilter(request)) {
                log.trace("Filter '{}' not yet executed.  Executing now.", this.getName());
                request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
                try {
                    this.doFilterInternal(request, response, filterChain);
                } finally {
                    request.removeAttribute(alreadyFilteredAttributeName);
                }
            } else {
                log.debug("Filter '{}' is not enabled for the current request.  Proceeding without invoking this filter.", this.getName());
                filterChain.doFilter(request, response);
            }
        }
    
        protected boolean isEnabled(ServletRequest request, ServletResponse response) throws ServletException, IOException {
            return this.isEnabled();
        }
    
        protected String getAlreadyFilteredAttributeName() {
            String name = this.getName();
            if (name == null) {
                name = this.getClass().getName();
            }
            return name + ".FILTERED";
        }
    
        /** @deprecated */
        @Deprecated
        protected boolean shouldNotFilter(ServletRequest request) throws ServletException {
            return false;
        }
        
        protected abstract void doFilterInternal(ServletRequest var1, ServletResponse var2, FilterChain var3) throws ServletException, IOException;
    }
    
    • 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
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    对比两个版本代码的发现:Apache Shiro = 1.12.0版本时,doFilter方法第三行代码增加了&& filterOncePerRequest判断,这个值就是通过ShiroFilterConfiguration > ShiroFilterFactoryBean一路传进来的,而且他是在构造ShiroFilterFactoryBean之后执行的, 比自定义Filter的构造时间要晚, 所以尝试在自定义过滤器的构造方法或者postxxx, afterxxx之类的方法中去设置为true都是没用的。

    只能是构造ShiroFilterFactoryBean对象时, 设置其配置属性来解决问题。

    解决方法

    • 创建ShiroFilterFactoryBean的时候, 给他一个ShiroFilterConfiguration实例对象, 并且设置这个实例的setFilterOncePerRequest(true)
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager)
        {
            ShiroFilterConfiguration config = new ShiroFilterConfiguration();
            //全局配置是否启用OncePerRequestFilter的只执行一次机制
            config.setFilterOncePerRequest(Boolean.TRUE);
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setShiroFilterConfiguration(config);
            // Shiro的核心安全接口,这个属性是必须的
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            // 身份认证失败,则跳转到登录页面的配置
            shiroFilterFactoryBean.setLoginUrl(loginUrl);
            // 权限认证失败,则跳转到指定页面
            shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
            // Shiro连接约束配置,即过滤链的定义
            LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
            // 对静态资源设置匿名访问
            ...........
            return shiroFilterFactoryBean;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    总结

    Apache Shiro = 1.9.0以前的版本,OncePerRequestFilter过滤器子类型默认只执行一次,
    现在可以通过全局配置来选择是否启用OncePerRequestFilter的只执行一次机制.

  • 相关阅读:
    软件开发详解:同城O2O与外卖跑腿系统源码的架构与开发要点
    vue--支付宝+微信--支付
    Vue中常用的指令
    打通Web安全思路:为什么我们需要同源策略?
    【全网最全】springboot整合JSR303参数校验与全局异常处理
    西煤交易平台竞拍学习
    maven的安装和配置
    【BOOST C++ 16 语言扩展】(4) Boost.Conversion
    【EMQX】使用websocket订阅EMQX数据
    设计模式-组合模式
  • 原文地址:https://blog.csdn.net/weixin_40750633/article/details/133162887