mag1c7
- 关注
漏洞描述
Apache Shiro before 1.10.0, Authentication Bypass Vulnerability in Shiro when forwarding or including via RequestDispatcher.
[1]
漏洞条件
shiro < 1.10.0
请求转发(forwarding)或者including:
存在一个不需要验证的路径转发(或including)到需要验证的路径。
漏洞复现
环境
shiro:1.9.1
springboot:2.7.4(tomcat 9.0.96)
shiro:
@Bean
public IniRealm getIniRealm() {
return new IniRealm("classpath:shiro.ini");
}
@Bean
public DefaultWebSecurityManager securityManager(IniRealm iniRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(iniRealm);
return manager;
}
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/login");
bean.setSuccessUrl("/loginSuccess");
bean.setUnauthorizedUrl("/unauthorized");
// URL 过滤规则
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("/withAuthcPage", "authc");
map.put("/login", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
//【补丁必要配置】!!!!
@Bean
public FilterRegistrationBean<AbstractShiroFilter> shiroFilterRegistration(ShiroFilterFactoryBean shiroFilterFactoryBean) throws Exception{
FilterRegistrationBean<AbstractShiroFilter> registrationBean = new FilterRegistrationBean<>();
// 获取 ShiroFilter 的实例
registrationBean.setFilter(shiroFilterFactoryBean.getObject());
// 设置 DispatcherType
registrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD);
return registrationBean;
}
最后一项配置等效于原生环境:
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
controller:注意controller不要用@RestController
,这会使得字符串关键字“forward”无效
//@Controller
//....
@GetMapping("/noAuthcPage")
public String noAuthcPage() {
return "forward:/withAuthcPage";
}
//以上方法等效于:
@GetMapping("/noAuthcPage")
public void noAuthcPage(HttpServletRequest request, HttpServletResponse response) {
try{
RequestDispatcher requestDispatcher = request.getRequestDispatcher("/withAuthcPage");
requestDispatcher.forward(request,response);
}catch (Exception e){
//ignore
}
}
@ResponseBody
@GetMapping("/withAuthcPage")
public String withAuthcPage() {
return "withAuthcPage";
}
测试:
漏洞分析
OncePerRequestFilter所在类图:
左边的分支是Shiro内部的过滤器,右分支AbstractShiroFilter才是正真注册在Web容器中的Filter,shiro-spring中注册到Web容器中的Filter是org.apache.shiro.spring.web.ShiroFilterFactoryBean$SpringShiroFilter
(implements AbstractShiroFilter)
OncePerRequestFilter.doFilter(...):省去日志相关内容
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
//漏洞点
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
filterChain.doFilter(request, response);
} else //noinspection deprecation
if (!isEnabled(request, response) ||
shouldNotFilter(request) ) {
filterChain.doFilter(request, response);
} else {
// Do invoke this filter...
//第一次会进入这个分支
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
//由子类实现
doFilterInternal(request, response, filterChain);
} finally {
// Once the request has finished, we're done and we don't
// need to mark as 'already filtered' any more.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
SpringShiroFilter第一次doFilter:
请求转发后SpringShiroFilter第二次进入:
由于请求转发后的请求本质上还是同一个请求对象,因此会进入图中分支。这里filterChain是ApplicationFilterChain,也就是web容器内的过滤链,filterChian.doFilter()
将会导致,filterChian直接跨过当前的SpringShiroFilter,进入下一个过滤器(或直接进入servlet),从而绕过shiro。
漏洞修复
改变了判定条件:[3]
但是这个补丁,是有限制的,必须要用上文环境部分的【补丁必要配置】,才能够生效
为什么要有【补丁必要配置】?
其实如果没有【补丁必要配置】,则转发的请求根本不会进入shiro,最终也能得到相同的测试结果。这是因为tomcat在请求转发后会重新生成生成过滤链,然后request重新经过filterChains-->servlet,如果没有该配置,则SpringShiroFilter不会被添加进这个过滤链,从而绕过shiro限制.[2]
核心类以及对应方法:
//org.apache.catalina.core.ApplicationDispatcher implements AsyncDispatcher, RequestDispatcher
private void invoke(ServletRequest request, ServletResponse response,
State state) throws IOException, ServletException {
// Checking to see if the context classloader is the current context
// classloader. If it's not, we're saving it, and setting the context
// classloader to the Context classloader
ClassLoader oldCCL = context.bind(false, null);
// Initialize local variables we may need
....
// Check for the servlet being marked unavailable
if (wrapper.isUnavailable()) {
......
}
// Allocate a servlet instance to process this request
try {
if (!unavailable) {
servlet = wrapper.allocate();
}
} catch (ServletException e) {
....
}
// Get the FilterChain Here
//如果没有【补丁必要配置】,则SpringShiroFilter不会被添加进去
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// Call the service() method for the allocated servlet instance
try {
// for includes/forwards
if ((servlet != null) && (filterChain != null)) {
filterChain.doFilter(request, response);
}
// Servlet Service Method is called by the FilterChain
} catch (ClientAbortException e) {
.....
}
//.....释放资源
读者可以自行尝试在大于等于shiro 1.10.0的版本中,不用【补丁必要配置】,会发生什么。本人在shiro 1.13.0条件下,不用【补丁必要配置】(其余不变),发现是可以绕过的。这里就不展开了
补充
【补丁必要配置】,可以不用手动配置,可以通过引入以下依赖,完成自动配置
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>对应版本</version>
</dependency>
其中具体配置类是org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration
Reference
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
