漏洞描述
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.0
、1.11.0
、1.12.0
,直到1.12.0版本该漏洞未出现了。
本以为是简单的版本升级,结果发现登录请求里的createToken
之类的方法每次都执行2次;
跟踪代码
@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;
}
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;
}
从上面方法可以看出来postProcessBeforeInitialization方法在bean初始化的时候去执行, 将自定义的登录过滤器中的setFilterOncePerRequest设置为了ShiroFilterConfiguration实例中给定的值;
其值默认是false,未启用OncePerRequestFilter的只执行一次机制
OncePerRequestFilter
类的核心方法(1.9.0和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;
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;
}
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;
}
对比两个版本代码的发现:Apache Shiro = 1.12.0版本时,doFilter方法第三行代码增加了&& filterOncePerRequest
判断,这个值就是通过ShiroFilterConfiguration
> ShiroFilterFactoryBean
一路传进来的,而且他是在构造ShiroFilterFactoryBean
之后执行的, 比自定义Filter的构造时间要晚, 所以尝试在自定义过滤器的构造方法或者postxxx
, afterxxx
之类的方法中去设置为true都是没用的。
只能是构造ShiroFilterFactoryBean
对象时, 设置其配置属性来解决问题。
解决方法
@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;
}
总结
Apache Shiro = 1.9.0以前的版本,OncePerRequestFilter
过滤器子类型默认只执行一次,
现在可以通过全局配置来选择是否启用OncePerRequestFilter
的只执行一次机制.