反序列化漏洞原理详解

Apache shiro 简介

Apache Shiro 是一个强大且易用的 Java 安全框架,执行身份验证、授权、密码和会话管理。使用 Shiro 的易于理解的 API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

本文针对 Shiro 进行了一个原理性的讲解,从源码层面来分析了 Shiro 的认证和授权的整个流程,并在认证与授权的这个流程讲解冲,穿插说明 rememberme 的作用,以及为何该字段会导致反序列化漏洞。

Apache shiro 认证

在该小节中我们将会详细讲解 Shiro 是如何认证一个用户为合法用户的 Shiro 漏洞环境测试代码修改自 Vulhub 中的 CVE-2016-4437。首先是 Shiro 的配置文件,代码如下所示

@Configuration
public class ShiroConfig {
    @Bean
    MainRealm mainRealm() {
        return new MainRealm();
    }

    @Bean
    RememberMeManager cookieRememberMeManager() {
        return (RememberMeManager)new CookieRememberMeManager();
    }

    @Bean
    SecurityManager securityManager(MainRealm mainRealm, RememberMeManager cookieRememberMeManager) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm((Realm)mainRealm);
        manager.setRememberMeManager(cookieRememberMeManager);
        return (SecurityManager)manager;
    }

    @Bean(name = {"shiroFilter"})
    ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
          //设置登录页面uri
        bean.setLoginUrl("/login");
          //设置登录失败页面uri
        bean.setUnauthorizedUrl("/unauth");

        Map map = new LinkedHashMap<>();
        map.put("/doLogin", "anon");
        map.put("/doLogout", "authc");
        map.put("/user/add","perms[user:add]");
        map.put("/user/update","perms[user:update]");
        map.put("/user/delete","perms[user:delete]");
        map.put("/user/select","perms[user:select]");
        map.put("/**", "authc");

        bean.setFilterChainDefinitionMap(map);

        return bean;
    }
}

小伙伴们有兴趣想了解更多相关学习资料请点赞收藏+评论转发+关注我之后私信我,注意回复【000】即可获取更多免费资料!

反序列化漏洞原理详解_第1张图片

 然后是 Controller 的代码

@Controller
public class UserController {
    @PostMapping({"/doLogin"})
    public String doLoginPage(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam(name = "rememberme", defaultValue = "") String rememberMe) {
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(username, password, rememberMe.equals("remember-me")));
        } catch (AuthenticationException e) {
            return "forward:/login";
        }
        return "forward:/";
    }

    @RequestMapping({"/doLogout"})
    public String doLogout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "forward:/login";
    }

    @RequestMapping({"/"})
    public String helloPage() {
        return "hello";
    }

    @RequestMapping({"/unauth"})
    public String errorPage() {
        return "error";
    }

    @RequestMapping({"/login"})
    public String loginPage() {
        return "loginUser";
    }

    @RequestMapping({"/user/add"})
    public String add(){
        return "/user/add";
    };

    @RequestMapping({"/user/delete"})
    public String delete(){
        return "/user/delete";
    };

    @RequestMapping({"/user/update"})
    public String update(){
        return "/user/update";
    };

    @RequestMapping({"/user/select"})
    public String select(){
        Subject subject = SecurityUtils.getSubject();
        return "/user/select";
    };

}

最后是 Realm

public class MainRealm extends AuthorizingRealm {

    @Autowired
    UserServiceImpl userService;

    /**该方法用来为登陆的用户进行授权*/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=>授权doGetAuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Subject subject = SecurityUtils.getSubject();
        System.out.println(subject.isAuthenticated());
        System.out.println(subject.isRemembered());
        if(!subject.isAuthenticated()){
            return null;
        }
        Users users = (Users) subject.getPrincipal();

        if(users.getPerm()!=null){
            String[] prems = users.getPerm().split(";");
            info.addStringPermissions(Arrays.asList(prems));
        }

        return info;
    }

    /**该方法用来校验登陆的用户*/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了=>认证doGetAuthenticationInfo");
        Subject subject= SecurityUtils.getSubject();
        System.out.println(subject.isAuthenticated());
        System.out.println(subject.isRemembered());
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
        String username = usernamePasswordToken.getUsername();
        char[] password = usernamePasswordToken.getPassword();

        Users users = userService.queryUserByName(username);

        if (users.getUsername()==null){
            return null;
        }
        return new SimpleAuthenticationInfo(users,users.getPassword(),"");

    }
}

这里来看一下自定义的 MainRealm 的类继承和实现关系图

反序列化漏洞原理详解_第2张图片

Realm 所起到的作用通常是获取后台用户的相关信息,然后获取前端传递进来的用户信息,将二者封装好然后交由 shiro 进行认证比对从而判断用户是否为合法用户,然后在用户访问后台资源时,为用户授予指定好的权限。那么认证是怎么认证的呢?下面来从 Shiro 源码的角度来进行详细的分析。首先是登陆页面,和登陆页面的代码。

反序列化漏洞原理详解_第3张图片

 当点击 Singn in 按钮的时候 后台对应的 Controller 就会执行但是在执行到 Controller 之前,Shiro 会进行一个操作,如下所示

反序列化漏洞原理详解_第4张图片

首先就是 Shiro 的 Filter,在 Shiro 的配置文件中,通过@Bean注解让 SpringBoot 在启动的时候自动装配了当前方法的返回值,也就是一个 ShiroFilterFactoryBean 对象,该对象的类继承关系如下所示。

反序列化漏洞原理详解_第5张图片

该类实现了 SpringFrameWork 中的 FactoryBean 接口和 BeanPostProcessor 接口。SpringBoot 在启动的时候会扫描当前目录以及子目录下所有.java 文件的注解,然后进行装配,这一过程中就会调用 FactoryBean.getObject()方法。也就是 FactoryBean 的实现类 ShiroFilterFactoryBean.getObject()方法,

反序列化漏洞原理详解_第6张图片

在 shiroFilter 的执行的堆栈中,会创建一个 Subject,Subject 是 Shiro 中很重要的一个概念,简单来说就是当前所操作的用户。当前线程中的用户所进行的认证和授权等等操作,都会以操作这个 Subject 对象来进行,所以 Subject 也被称之为主体,最终实例化的是一个 WebDelegatingSubject 对象。请求继续往下执行,来到 UserController.doLoginPage()方法,该方法中会调用 Subject.login()方法,并传入一个 UsernamePasswordToken 对象。这个 UsernamePasswordToken 从这个类的名字我们就可以猜出这个类是用来做什么的,跟进该类中看一下

反序列化漏洞原理详解_第7张图片

从这个类提供的方法和属性就可以看出来,UsernamePasswordToken 类就是一个单纯的 pojo 类,登陆时的用户名和密码以及对应的 ip 信息都会在这个类中暂时存放。跟进 Subject.login()方法,经过一系列的调用来到了 ModularRealmAuthenticator.doAuthenticate,该方法会获取我们自定义的 Realm 并一次进行调用,我们自定义的 Realm 是文章开头的 MainRealm,所谓的 Realm,就是对传入的用户进行认证和授权的地方,Realm 的自定义需要继承自 AuthorizingRealm,Realm 我们可以自定义多个,只需要将自定义好的多个 Realm 放入一个 Collection 对象中,然后在配置文件中通过 SecurityManager.setRealms()传入,这样在 Shiro 在认证时就会依次调用我们自定义的 Realms,Shiro 本身也自带有一些 Reamls 可以直接调用,如下图所示

反序列化漏洞原理详解_第8张图片

自定义的 Realm 有两个方法必须要实现,分别是继承自 AuthencationgRealm 的 doGetAuthenticationInfo()方法,和 AuthorizingRealm 的 doGetAuthorizationInfo 方法,如下图所示

反序列化漏洞原理详解_第9张图片

下面根据程序执行流程,先讲 doGetAuthenticationInfo,根据之前所讲调用 subject.login()方法时会调用到我们自定义的 Realm 的 doGetAuthenticationInfo 方法,我们在该方法中的实现非常简单,即从后台数据库中根据用户名进行查询用户是否存在,如果存在则将查询出来的数据封装成 Users 对象,然后将封装好的 Users 对象传入和查询出的该用户的密码一同传入 SimpleAuthenticationInfo 类构造方法中并进行返回。这一步说是用来进行用户的认证,但是不难发现,该方法中并没有对用户的密码进行校验,那么真正的校验点在哪里呢,在如下图所示的位置

反序列化漏洞原理详解_第10张图片

在 AuthenticatingRealm 的 getAuthenticationInfo 方法中不仅调用了我们自定义的 MainRealm 中的 doGetAuthenticationInfo 方法,还调用了自身的 assertCredentialsMatch 方法,如下图所示,而 assertCredentialsMatch 方法就是用来校验前端传递来的用户名和密码,以及后台从数据库查询出的密码进行比对的。

反序列化漏洞原理详解_第11张图片

在 assertCredentialsMatch 方法中跟如 cm.doCredentialsMatch(token, info),然后就可以看到 shiro 如何进行用户密码比对的了。

反序列化漏洞原理详解_第12张图片

token 是前端传入的用户名和密码封装成的 UsernamePasswordToken 对象,info 是从数据库中查询出的数据封装成的 SimpleAuthenticationInfo 对象,如此一来,获取二者的密码,进行 equals 比对,相同则程序继续执行,不相同则抛出异常,返回登陆界面。那么 Shiro 认证到这里就结束了么?当然不是,之前提到过,Shiro 中有一个概念叫 Subject,Subject 代表的就是用户当前操作的主体,在这第一次登陆认证中我们也是通过调用了一个 Subject 对象的 login 方法才进行的身份验证,但是在这个 Subject 中是没有任何的用户信息的,当用户的信息通过校验之后,Shiro 又会实例化一个 WebDelegatingSubject,而这个位置就在 DefaultSecurityManager 的 login 方法中,如下图所示

反序列化漏洞原理详解_第13张图片

我们之前看到的认证过程就在 authenticate 方法里,身份真正成功后会返回用户的信息,封装在一个 SimplePrincipalCollection 对象里,如果认证失败,则会抛出异常。认证成功后,Shiro 就会根据当前用户的一些信息,再创建一个 Subject,后续该用户进行的任何操作都会以这个 Subject 为主,授权也是 Shiro 给这个 Subject 进行授权。如此以来我们就了解了 Shiro 是如何认证一个用户的,下面来总结一下 Shiro 认证用户的一个思路,首先在用户没有进行认证的时候访问一些资源,Shiro 会生成一个 Subject,这个 Subject 没有任何的用户信息。当用户开始登陆,Shiro 会调用 Subject 的 login 方法,对用户的用户名和密码进行校验,校验通过后,会生成一个新的 Subject,后续用户的授权等操作,都会基于这个新生成的 Subject。

Apache Shiro 授权

看完了 Shiro 的认证过程,接下来我们来看 Shiro 的授权过程。我们将每位用户所拥有的授权都存入数据库中如下所示

反序列化漏洞原理详解_第14张图片

这里以 admin 为例,来分析下 Shiro 授权的过程。书接上文 Shiro 认证部分,认证完成功 Shiro 会生成一个新的 Subject,而 shiro 的授权过程也就是围绕着这个 Subject 来进行的,那么 Shiro 何时会为用户进行授权行为呢?在之前的内容中说过,自定义的 Realm 有两个方法必须要实现,分别是继承自 AuthencationgRealm 的 doGetAuthenticationInfo()方法,和 AuthorizingRealm 的 doGetAuthorizationInfo 方法。doGetAuthenticationInfo()方法我们已经清楚了,是用来进行用户身份验证的,而 doGetAuthorizationInfo()方法就是用来进行用户授权的。再回顾一下之前的配置文件中我们为每个资源所授予的访问权限,权限如下所示。

 @Bean(name = {"shiroFilter"})
    ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        bean.setLoginUrl("/login");
        bean.setUnauthorizedUrl("/unauth");
        /**
         * anon:无需认证就可以访问
         * authc: 必须认证了才能访问
         * user: 必须拥有记住我功能才能访问
         * perms: 拥有对某个资源的权限才能访问
         * role: 拥有某个角色权限才能访问
         * */
        Map map = new LinkedHashMap<>();
        map.put("/doLogin", "anon");
        map.put("/doLogout", "authc");
        map.put("/user/add","perms[user:add]");
        map.put("/user/update","perms[user:update]");
        map.put("/user/delete","perms[user:delete]");
        map.put("/user/select","perms[user:select]");
        map.put("/**", "authc");

        bean.setFilterChainDefinitionMap(map);

        return bean;
    }

我们在 doGetAuthorizationInfo()方法中下断点,当已经经过认证的用户访问制定资源的时候,shiro 就会调用 doGetAuthorizationInfo()方法来为该用户进行授权,具体怎么执行到该方法的就不细说了,将调用链粘贴一下。

反序列化漏洞原理详解_第15张图片

doGetAuthorizationInfo()方法的具体实现如下所示

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String name = getName();
        System.out.println("执行了=>授权doGetAuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
      //获取当前用户的subject
        Subject subject = SecurityUtils.getSubject();
        System.out.println(subject.isAuthenticated());
        System.out.println(subject.isRemembered());
        if(!subject.isAuthenticated()){
            return null;
        }
//        Users users = userService.queryUserByName((String) subject.getPrincipal());
      //
      //获取当前用户的信息
        Users users = (Users) subject.getPrincipal();
            //判断当前用户的权限字段是否为空,如果不为空的话就传入SimpleAuthorizationInfo的addStringPermissions方法中。
        if(users.getPerm()!=null){
            String[] prems = users.getPerm().split(";");
            info.addStringPermissions(Arrays.asList(prems));
        }

        return info;
    }

 之前在认证的那一步中,我们将数据库中的数据封装成一个 Users 对象,该对象存放入了 Subject 中,doGetAuthorizationInfo()方法中我们将其取出。在该方法中,我们所做的只是将用户数据库中的权限字段取出然后封装入一个 SimpleAuthorizationInfo 对象中,并进行返回,我们跟随看一下 Shiro 后续的操作。在获取完当前用户的权限后,堆栈返回到 AuthorizingRealm 的 isPermitted 方法中,该方法又调用了 isPermitted()方法,isPermitted()方法就是用来判断用户是否有权限访问指定资源的方法。isPermitted()方法具体内容如下所示

反序列化漏洞原理详解_第16张图片

该方法会讲用户所拥有的权限循环遍历出来,然后和当前资源所需要访问权限进行一一比对,如果相同则返回 true。那么比对规则是怎样的呢?跟进 implies()方法,内容如下所示

反序列化漏洞原理详解_第17张图片

这里简述一下比对的规则,当前资源所需的访问权限[user:add],对于 shiro 来说所谓的访问权限不过就是一串字符串而已,shiro 会将[user:add]以“:”进行分割,分割成 user 和 add 两个字符串,而假如用户具有[user:add],和[user:select]这两个权限,第一次循环就是判断[user:select]和[user:add]是否相同,会首先判断“:”之前的字符串是否相同,也就是 user 这部分是否相同,相同则继续,不相同则返回 false。判断相同以后,会第二次循环判断“:”之后的部分是否相同,也就是 add 和 select。那自然是不相同的,所以返回 false。shiro 接下来会继续判断[user:add]和[user:add]是否相同。这就是 shiro 授权和鉴权的代码流程,也是 shiro 的核心。了解了 shiro 的这部分内容之后,我们接下来就该讲 CVE-2016-4437 这个漏洞的具体内容了。

Apahce Shiro 反序列化漏洞的根源

shiro 在用户登陆的时候,除了用户名和密码以外 还有一个可传递的选项,也就是 shiro 发序列化漏洞产生的根源,Rememberme。Rememberme 的核心作用时什么呢?就是用户在登录时勾选 rememberme 选项,Cookie 中就会增加一个 rememberme 字段,该字段中会存储一些序列化数据,开发者可以指定 rememberme 字段的有效时间,同时开发者可以指定一些资源,这些资源允许携带 rememberme 字段的用户访问,由于 rememberme 是存储在浏览器中的,并在用户的每一次请求中被携带,所以只要不清除 Cookie,用户就可以在 rememberme 的有效时间内,无需再次登陆,就可以访问指定资源。在不勾选 rememberme 的情况下,通常就是浏览器关闭,会话就会立刻结束,活着等待一段时间后结束,届时用户想要访问一些资源则需要重新登陆,勾选 rememberme 后,即使推出浏览器结束与服务端的会话,rememberme 仍然存储在浏览器中,重新打开浏览器访问指定资源,浏览器在请求时仍会携带上 rememberme,如此一来就不需要重新登陆了。那么接下来就来分析 rememberme 是如何生成,以及如何实现无需登录即可访问指定资源的。如果登陆的时候不勾选 rememberme 选项的情况下,Shiro 是不会生成 rememberme 的,勾选了 rememberme 选项后,才会在认证的过程中生成该值。生成 rememberme 的位置在 DefaultSecurityManager 的 login 方法中,如下图所示。

反序列化漏洞原理详解_第18张图片

位置就是在 Shiro 完成用户认证,生成一个新的 Subject 之后。跟进 onSuccessfulLogin()方法,经过嵌套调用,来到 AbstractRememberMeManager 的 onSuccessfulLogin 方法,

反序列化漏洞原理详解_第19张图片

在该方法中,会先判断此次请求中 remebmberme 字段是否存在,如果存在则调用 rememberIdentity()方法,想要知道 rememberme 中存储了什么东西那么就要继续深入。

这里是获取到了一个 PrincipalCollection 对象,继续深入。

接下来就是将这个 PrincipalCollection 转化成 byte 数组。这个方法很关键,我们需要跟入看一下

反序列化漏洞原理详解_第20张图片

看到这里大家应该就明白了,Shiro 为什么会有反序列化漏洞,以及 rememberme 所传递的数据就究竟是什么,其实就是一个序列化后的 PrincipalCollection 对象,而这个 encrypt 就是通过 AES 来加密序列化后的数据,密钥呢?当然就是硬编码在 AbstractRememberMeManager 类中的这段 base64 编码后的字符串了,如下图所示

最后的最后,Shiro 会将这段加密后的数据 base64 编码一遍,然后放入 Cookie 中,至此 Shiro 生成 rememberme 的过程就结束了

反序列化漏洞原理详解_第21张图片

那么知道了 rememberme 是怎么加密生成的,那么自然也就可以很轻易的解密了,尤其还是在密钥硬编码在代码中的这种情况,下面是解密的 demo

反序列化漏洞原理详解_第22张图片

那么反序列化漏洞产生的点在哪里呢?当用户在登录时勾选了 rememberme 的时候,Shiro 会返回一个 rememberme 通过 Cookie 字段传递,然后存储在浏览器中,正常情况下当用户关闭浏览器或者手动删除浏览器中存储的 SesseionID 的时候,与服务端的当前会话就结束了,当下次打开浏览器再此访问服务端的时候,就需要重新登录。而勾选了 rememberme 的用户登录后,关闭浏览器后,会话同样会关闭,但是下次打开浏览器请求访问服务端的时候,Cookie 中会携带 Rememberme 来进行请求,从而达到无需登录的效果。漏洞的产生关键就在于再重新建立会话的这个过程,所以想要触发 rememberme 的话请求包中就不能有 SessionID,删除 SessionID 后再携带 rememberme 进行一次请求就可以触发 rememberme 的反序列化点,如下图所示

反序列化漏洞原理详解_第23张图片

最终通过 base64 解码,然后 AES 解密,解密后的结果再经过反序列化,就还原成了一个 PrincipalCollection 对象,至此 Shiro rememberme 的生成以及作用,还有如何触发 rememberme 反序列化,都已讲解完毕。

反序列化漏洞原理详解_第24张图片

总结

Apache Shiro 是一款相当优秀的认证授权框架,虽然在护网等大型攻防项目中,经常被作为突破口,但是仍然瑕不掩瑜,shiro 的反序列化在流量识别中是比较容易判断的,因为序列化数据的传递必须要通过 Cookie 中的 rememberme 字段,但是纵使识别出来,但是如果不知道密钥的话,也无法得知传递的内容。

你可能感兴趣的:(后端,java,安全,web安全,系统架构,开发语言)