专栏名称: 天融信阿尔法实验室
天融信阿尔法实验室将不定期推出技术研究新方向成果,专注安全攻防最前沿技术
目录
相关文章推荐
新浪科技  ·  【#专家谈人形机器人是否伪需求##专家称人形 ... ·  12 小时前  
新浪科技  ·  转发微博-20250314192012 ·  13 小时前  
51好读  ›  专栏  ›  天融信阿尔法实验室

Apache Shiro反序列化漏洞详解

天融信阿尔法实验室  · 公众号  ·  · 2021-12-08 15:27

正文



0x01 Apache shiro简介


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

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


0x02 Apache shiro认证

在该小节中我们将会详细讲解Shiro是如何认证一个用户为合法用户的

Shiro漏洞环境测试代码修改自Vulhub中的CVE-2016-4437。

首先是Shiro的配置文件,代码如下所示

@Configurationpublic 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; }}

然后是Controller的代码

@Controllerpublic 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的类继承和实现关系图

Realm所起到的作用通常是获取后台用户的相关信息,然后获取前端传递进来的用户信息,将二者封装好然后交由shiro进行认证比对从而判断用户是否为合法用户,然后在用户访问后台资源时,为用户授予指定好的权限。

那么认证是怎么认证的呢?下面来从Shiro源码的角度来进行详细的分析。

首先是登陆页面,和登陆页面的代码。

当点击Singn in按钮的时候 后台对应的Controller就会执行

但是在执行到Controller之前,Shiro会进行一个操作,如下所示

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

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

在shiroFilter的执行的堆栈中,会创建一个Subject,Subject是Shiro中很重要的一个概念,简单来说就是当前所操作的用户。当前线程中的用户所进行的认证和授权等等操作,都会以操作这个Subject对象来进行,所以Subject也被称之为主体,最终实例化的是一个WebDelegatingSubject对象。

请求继续往下执行,来到UserController.doLoginPage()方法,该方法中会调用Subject.login()方法,并传入一个UsernamePasswordToken 对象。这个UsernamePasswordToken从这个类的名字我们就可以猜出这个类是用来做什么的,跟进该类中看一下

从这个类提供的方法和属性就可以看出来,UsernamePasswordToken类就是一个单纯的pojo类,登陆时的用户名和密码以及对应的ip信息都会在这个类中暂时存放。

跟进Subject.login()方法,经过一系列的调用来到了ModularRealmAuthenticator.doAuthenticate,该方法会获取我们自定义的Realm并一次进行调用,我们自定义的Realm是文章开头的MainRealm,所谓的Realm,就是对传入的用户进行认证和授权的地方,Realm的自定义需要继承自AuthorizingRealm,Realm我们可以自定义多个,只需要将自定义好的多个Realm放入一个Collection对象中,然后在配置文件中通过SecurityManager.setRealms()传入,这样在Shiro在认证时就会依次调用我们自定义的Realms,Shiro本身也自带有一些Reamls可以直接调用,如下图所示

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

下面根据程序执行流程,先讲doGetAuthenticationInfo,根据之前所讲调用subject.login()方法时会调用到我们自定义的Realm的doGetAuthenticationInfo方法,我们在该方法中的实现非常简单,即从后台数据库中根据用户名进行查询用户是否存在,如果存在则将查询出来的数据封装成Users对象,然后将封装好的Users对象传入和查询出的该用户的密码一同传入SimpleAuthenticationInfo类构造方法中并进行返回。

这一步说是用来进行用户的认证,但是不难发现,该方法中并没有对用户的密码进行校验,那么真正的校验点在哪里呢,在如下图所示的位置

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

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

token是前端传入的用户名和密码封装成的UsernamePasswordToken对象,info是从数据库中查询出的数据封装成的SimpleAuthenticationInfo对象,如此一来,获取二者的密码,进行equals比对,相同则程序继续执行,不相同则抛出异常,返回登陆界面。

那么Shiro认证到这里就结束了么?当然不是,之前提到过,Shiro中有一个概念叫Subject,Subject代表的就是用户当前操作的主体,在这第一次登陆认证中我们也是通过调用了一个Subject对象的login方法才进行的身份验证,但是在这个Subject中是没有任何的用户信息的,当用户的信息通过校验之后,Shiro又会实例化一个WebDelegatingSubject,而这个位置就在DefaultSecurityManager的login方法中,如下图所示

我们之前看到的认证过程就在authenticate方法里,身份真正成功后会返回用户的信息,封装在一个SimplePrincipalCollection对象里,如果认证失败,则会抛出异常。

认证成功后,Shiro就会根据当前用户的一些信息,再创建一个Subject,后续该用户进行的任何操作都会以这个Subject为主,授权也是Shiro给这个Subject进行授权。

如此以来我们就了解了Shiro是如何认证一个用户的,下面来总结一下Shiro认证用户的一个思路,首先在用户没有进行认证的时候访问一些资源,Shiro会生成一个Subject,这个Subject没有任何的用户信息。当用户开始登陆,Shiro会调用Subject的login方法,对用户的用户名和密码进行校验,校验通过后,会生成一个新的Subject,后续用户的授权等操作,都会基于这个新生成的Subject。







请到「今天看啥」查看全文