14日晚上,发现了一个公开的Shiro的RCE漏洞,作为在甲方工作了挺久的人,一听Shiro立马虎躯一震,来不及分析poc,立马先得确认公司业务是否受到影响。确定了影响后,立马看怎么修复漏洞。结果悲催的时候出现了,这个漏洞官方居然没有升级修复。这让笔者想起差不多半年前给Shiro官方推的一个加固,结果一直没有收到官方回复,直接杳无音讯。
以下组件的全量版本均收到漏洞影响,当然这个这个漏洞需要可以登录才能利用。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</dependency>
参考https://www.anquanke.com/post/id/192819。
笔者看漏洞分析,大致清楚了漏洞原理及其触发条件,但来不及分析更多细节。也更加聚焦于怎么让业务修复这个漏洞。大致想了下几种常见的漏洞处理办法:
(1)升级:上面说了官方没有解决,pass
(2)下线业务:除非业务真的没有用了,否则面谈,pass
(3)WAF拦截EXP:Shiro的EXP是加密值,特征不够明显(这里先不谈绕过),而且有些甲方的有些业务还没接WAF,pass。
如此看来,似乎只能希望攻击者没有账号不来搞事情了。但是想想,这么搞也太不负责任,并且业务还在等着方案,通知了业务有没有修复方案难免有点有损安全部门门脸。是在没有办法,只能想着自己出修复方案了。
经过和业务沟通,发现有些业务虽然用了Shiro框架,但是并不需要rememberMe这个功能,于是想着,能不能找到个配置方法,把这个功能直接干掉,经过分析,在极短的时间内没有发现。于是又想,Shiro这个漏洞不是从Filter触发的吗,能不能在Filter做点事情,把rememberMe这个功能干掉。于是,有两个思路来干掉rememberMe功能。
思路(1):提供一个Filter,在Shiro的Filter被调用前就调用,然后将cookie里的rememberMe直接置空。
思路(2):覆盖Shiro Filter的一些行为,然后去掉rememberMe功能。
思路(1)实现起来很简单,但是有个问题,就怕业务配置filer的顺序错误,导致问题没有修复。所有想着用思路(2)。
于是问题聚焦于怎么找到Shiro的Filter,以及怎么覆盖其行为。
用poc直接大断点,发现Filter是ShiroFilterFactoryBean$SpringShiroFilter。
但是发现这个内部类是个final Class,因此是没有办法被继承的。
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);
}
}
}
由于这个内部类是个final Class,只能去更底一级覆盖。我们看下Shiro的配置:
<filter>
<!--filter 配置-->
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--省略-->
</bean>
于是,我们发现可以复写ShiroFilterFactoryBean类,然后让业务使用我们配置的类,从而覆盖Shiro Filter默认的行为,去除rememberMe功能。于是得到以下的临时pactch方案。
这里强调几个注意点:(1)复写的类出得request是ServletRequest实例,没有操作cookie的方法。实际上在tomcat下,这个类是RequestFacade实例。(2)RequestFacade实例写代码时需要加额外的provided maven依赖。由于这个类和tomcat版本有关,因此需要测兼容性,不通用,可以用反射方式解决。
(1)在源码中添加以下类
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.springframework.beans.factory.BeanInitializationException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Field;
/**
* @author: Venscor
* @date: 2019/11/14
* @description
*/
public class SafeShiroFilterFactoryBean extends ShiroFilterFactoryBean {
@Override
protected AbstractShiroFilter createInstance() throws Exception {
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();
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter implementations
// do not know about FilterChainManagers - only resolvers:
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
//injection of the SecurityManager and FilterChainResolver:
return new SafeShiroFilterFactoryBean.SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
private static final class SpringShiroFilter extends AbstractShiroFilter {
protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
if (webSecurityManager == null) {
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
} else {
this.setSecurityManager(webSecurityManager);
if (resolver != null) {
this.setFilterChainResolver(resolver);
}
}
}
@Override
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
Cookie[] cookies = request.getCookies();
boolean needResetCookie = false;
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase("rememberMe")) {
cookie.setValue("");
needResetCookie = true;
break;
}
}
}
if (needResetCookie) {
Object innerReq = getField(request, "request");
setField(innerReq, "cookies", cookies);
setField(request, "request", innerReq);
}
super.doFilterInternal(servletRequest, servletResponse, chain);
}
public void setField(Object instance, String fieldName, Object fieldValue) {
try {
Field field = instance.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(instance, fieldValue);
} catch (Exception e) {
throw new RuntimeException();
}
}
public static Object getField(Object instance, String fieldName) {
try {
Field field = instance.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(instance);
} catch (Exception e) {
throw new RuntimeException();
}
}
}
}
(2)替换原始的ShiroFilter Bean
SpringMVC中配置示例:
<filter>
<!--filter 配置-->
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<!--替换原始的Shiro Bean配置-->
<bean id="shiroFilter" class="[包名].SafeShiroFilterFactoryBean">
<!--中间的配置不需要做任何改变-->
</bean>
SpringBoot配置示例:在原来配置Shiro的Config类中修改
@Bean(name = "shiroFilter")
//返回值修改:ShiroFilterFactoryBean改为SafeShiroFilterFactoryBean
public SafeShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager,
CasFilter casFilter) {
//这个地方改成SafeShiroFilterFactoryBean
//ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 这个地方替换掉
SafeShiroFilterFactoryBean shiroFilterFactoryBean = new SafeShiroFilterFactoryBean(); // 这个地方替换掉
shiroFilterFactoryBean.setSecurityManager(securityManager);
String loginUrl = cas.getServerUrlPrefix() + "/login?service=" + cas.getClientHostUrl() + cas.getCasFilterUrlPattern();
shiroFilterFactoryBean.setLoginUrl(loginUrl);
shiroFilterFactoryBean.setSuccessUrl("/");
// 未授权 url
shiroFilterFactoryBean.setUnauthorizedUrl(markProperties.getShiro().getUnauthorizedUrl());
Map<String, Filter> filters = new HashMap<>();
filters.put("casFilter", casFilter);
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setRedirectUrl(cas.getServerUrlPrefix() + "/logout?");
filters.put("logout", logoutFilter);
shiroFilterFactoryBean.setFilters(filters);
loadShiroFilterChain(shiroFilterFactoryBean);
return shiroFilterFactoryBean;
}
Shiro还有其他配置方式,可以类似处理。笔者水平有限,如果不当之处,还望不吝指正。