Apacheshiro简介 ApacheShiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。 本文针对Shiro进行了一个原理性的讲解,从源码层面来分析了Shiro的认证和授权的整个流程,并在认证与授权的这个流程讲解冲,穿插说明rememberme的作用,以及为何该字段会导致反序列化漏洞。Apacheshiro认证 在该小节中我们将会详细讲解Shiro是如何认证一个用户为合法用户的 Shiro漏洞环境测试代码修改自Vulhub中的CVE20164437。 首先是Shiro的配置文件,代码如下所示ConfigurationpublicclassShiroConfig{BeanMainRealmmainRealm(){returnnewMainRealm();}BeanRememberMeManagercookieRememberMeManager(){return(RememberMeManager)newCookieRememberMeManager();}BeanSecurityManagersecurityManager(MainRealmmainRealm,RememberMeManagercookieRememberMeManager){DefaultWebSecurityManagermanagernewDefaultWebSecurityManager();manager。setRealm((Realm)mainRealm);manager。setRememberMeManager(cookieRememberMeManager);return(SecurityManager)}Bean(name{shiroFilter})ShiroFilterFactoryBeanshiroFilterFactoryBean(SecurityManagersecurityManager){ShiroFilterFactoryBeanbeannewShiroFilterFactoryBean();bean。setSecurityManager(securityManager);设置登录页面uribean。setLoginUrl(login);设置登录失败页面uribean。setUnauthorizedUrl(unauth);MapString,StringmapnewLinkedHashMap();map。put(doLogin,anon);map。put(doLogout,authc);map。put(useradd,perms〔user:add〕);map。put(userupdate,perms〔user:update〕);map。put(userdelete,perms〔user:delete〕);map。put(userselect,perms〔user:select〕);map。put(,authc);bean。setFilterChainDefinitionMap(map);}} 【一所有资源关注我,私信回复资料获取一】 1、200份很多已经买不到的绝版电子书 2、30G安全大厂内部的视频资料 3、100份src文档 4、常见安全面试题 5、ctf大赛经典题目解析 6、全套工具包 7、应急响应笔记 8、网络安全学习路线 然后是Controller的代码ControllerpublicclassUserController{PostMapping({doLogin})publicStringdoLoginPage(RequestParam(username)Stringusername,RequestParam(password)Stringpassword,RequestParam(namerememberme,defaultValue)StringrememberMe){SubjectsubjectSecurityUtils。getSubject();try{subject。login(newUsernamePasswordToken(username,password,rememberMe。equals(rememberme)));}catch(AuthenticationExceptione){returnforward:}returnforward:;}RequestMapping({doLogout})publicStringdoLogout(){SubjectsubjectSecurityUtils。getSubject();subject。logout();returnforward:}RequestMapping({})publicStringhelloPage(){}RequestMapping({unauth})publicStringerrorPage(){}RequestMapping({login})publicStringloginPage(){returnloginU}RequestMapping({useradd})publicStringadd(){};RequestMapping({userdelete})publicStringdelete(){};RequestMapping({userupdate})publicStringupdate(){};RequestMapping({userselect})publicStringselect(){SubjectsubjectSecurityUtils。getSubject();};} 最后是RealmpublicclassMainRealmextendsAuthorizingRealm{AutowiredUserServiceImpluserS该方法用来为登陆的用户进行授权OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollectionprincipalCollection){System。out。println(执行了授权doGetAuthorizationInfo);SimpleAuthorizationInfoinfonewSimpleAuthorizationInfo();SubjectsubjectSecurityUtils。getSubject();System。out。println(subject。isAuthenticated());System。out。println(subject。isRemembered());if(!subject。isAuthenticated()){}Usersusers(Users)subject。getPrincipal();if(users。getPerm()!null){String〔〕premsusers。getPerm()。split(;);info。addStringPermissions(Arrays。asList(prems));}}该方法用来校验登陆的用户OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokenauthenticationToken)throwsAuthenticationException{System。out。println(执行了认证doGetAuthenticationInfo);SubjectsubjectSecurityUtils。getSubject();System。out。println(subject。isAuthenticated());System。out。println(subject。isRemembered());UsernamePasswordTokenusernamePasswordToken(UsernamePasswordToken)authenticationTStringusernameusernamePasswordToken。getUsername();char〔〕passwordusernamePasswordToken。getPassword();UsersusersuserService。queryUserByName(username);if(users。getUsername()null){}returnnewSimpleAuthenticationInfo(users,users。getPassword(),);}} 这里来看一下自定义的MainRealm的类继承和实现关系图 Realm所起到的作用通常是获取后台用户的相关信息,然后获取前端传递进来的用户信息,将二者封装好然后交由shiro进行认证比对从而判断用户是否为合法用户,然后在用户访问后台资源时,为用户授予指定好的权限。 那么认证是怎么认证的呢?下面来从Shiro源码的角度来进行详细的分析。 首先是登陆页面,和登陆页面的代码。 当点击Singnin按钮的时候后台对应的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。ApacheShiro授权 看完了Shiro的认证过程,接下来我们来看Shiro的授权过程。 我们将每位用户所拥有的授权都存入数据库中如下所示 这里以admin为例,来分析下Shiro授权的过程。 书接上文Shiro认证部分,认证完成功Shiro会生成一个新的Subject,而shiro的授权过程也就是围绕着这个Subject来进行的,那么Shiro何时会为用户进行授权行为呢? 在之前的内容中说过,自定义的Realm有两个方法必须要实现,分别是继承自AuthencationgRealm的doGetAuthenticationInfo()方法,和AuthorizingRealm的doGetAuthorizationInfo方法。 doGetAuthenticationInfo()方法我们已经清楚了,是用来进行用户身份验证的,而doGetAuthorizationInfo()方法就是用来进行用户授权的。 再回顾一下之前的配置文件中我们为每个资源所授予的访问权限,权限如下所示。Bean(name{shiroFilter})ShiroFilterFactoryBeanshiroFilterFactoryBean(SecurityManagersecurityManager){ShiroFilterFactoryBeanbeannewShiroFilterFactoryBean();bean。setSecurityManager(securityManager);bean。setLoginUrl(login);bean。setUnauthorizedUrl(unauth);anon:无需认证就可以访问authc:必须认证了才能访问user:必须拥有记住我功能才能访问perms:拥有对某个资源的权限才能访问role:拥有某个角色权限才能访问MapString,StringmapnewLinkedHashMap();map。put(doLogin,anon);map。put(doLogout,authc);map。put(useradd,perms〔user:add〕);map。put(userupdate,perms〔user:update〕);map。put(userdelete,perms〔user:delete〕);map。put(userselect,perms〔user:select〕);map。put(,authc);bean。setFilterChainDefinitionMap(map);} 我们在doGetAuthorizationInfo()方法中下断点,当已经经过认证的用户访问制定资源的时候,shiro就会调用doGetAuthorizationInfo()方法来为该用户进行授权,具体怎么执行到该方法的就不细说了,将调用链粘贴一下。 doGetAuthorizationInfo()方法的具体实现如下所示protectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollectionprincipalCollection){StringnamegetName();System。out。println(执行了授权doGetAuthorizationInfo);SimpleAuthorizationInfoinfonewSimpleAuthorizationInfo();获取当前用户的subjectSubjectsubjectSecurityUtils。getSubject();System。out。println(subject。isAuthenticated());System。out。println(subject。isRemembered());if(!subject。isAuthenticated()){}UsersusersuserService。queryUserByName((String)subject。getPrincipal());获取当前用户的信息Usersusers(Users)subject。getPrincipal();判断当前用户的权限字段是否为空,如果不为空的话就传入SimpleAuthorizationInfo的addStringPermissions方法中。if(users。getPerm()!null){String〔〕premsusers。getPerm()。split(;);info。addStringPermissions(Arrays。asList(prems));}} 之前在认证的那一步中,我们将数据库中的数据封装成一个Users对象,该对象存放入了Subject中,doGetAuthorizationInfo()方法中我们将其取出。 在该方法中,我们所做的只是将用户数据库中的权限字段取出然后封装入一个SimpleAuthorizationInfo对象中,并进行返回,我们跟随看一下Shiro后续的操作。 在获取完当前用户的权限后,堆栈返回到AuthorizingRealm的isPermitted方法中,该方法又调用了isPermitted()方法,isPermitted()方法就是用来判断用户是否有权限访问指定资源的方法。 isPermitted()方法具体内容如下所示 该方法会讲用户所拥有的权限循环遍历出来,然后和当前资源所需要访问权限进行一一比对,如果相同则返回true。那么比对规则是怎样的呢? 跟进implies()方法,内容如下所示 这里简述一下比对的规则,当前资源所需的访问权限〔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的这部分内容之后,我们接下来就该讲CVE20164437这个漏洞的具体内容了。ApahceShiro反序列化漏洞的根源 shiro在用户登陆的时候,除了用户名和密码以外还有一个可传递的选项,也就是shiro发序列化漏洞产生的根源,Rememberme。 Rememberme的核心作用时什么呢?就是用户在登录时勾选rememberme选项,Cookie中就会增加一个rememberme字段,该字段中会存储一些序列化数据,开发者可以指定rememberme字段的有效时间,同时开发者可以指定一些资源,这些资源允许携带rememberme字段的用户访问,由于rememberme是存储在浏览器中的,并在用户的每一次请求中被携带,所以只要不清除Cookie,用户就可以在rememberme的有效时间内,无需再次登陆,就可以访问指定资源。在不勾选rememberme的情况下,通常就是浏览器关闭,会话就会立刻结束,活着等待一段时间后结束,届时用户想要访问一些资源则需要重新登陆,勾选rememberme后,即使推出浏览器结束与服务端的会话,rememberme仍然存储在浏览器中,重新打开浏览器访问指定资源,浏览器在请求时仍会携带上rememberme,如此一来就不需要重新登陆了。 那么接下来就来分析rememberme是如何生成,以及如何实现无需登录即可访问指定资源的。 如果登陆的时候不勾选rememberme选项的情况下,Shiro是不会生成rememberme的,勾选了rememberme选项后,才会在认证的过程中生成该值。 生成rememberme的位置在DefaultSecurityManager的login方法中,如下图所示。 位置就是在Shiro完成用户认证,生成一个新的Subject之后。跟进onSuccessfulLogin()方法,经过嵌套调用,来到AbstractRememberMeManager的onSuccessfulLogin方法, 在该方法中,会先判断此次请求中remebmberme字段是否存在,如果存在则调用rememberIdentity()方法,想要知道rememberme中存储了什么东西那么就要继续深入。 这里是获取到了一个PrincipalCollection对象,继续深入。 接下来就是将这个PrincipalCollection转化成byte数组。这个方法很关键,我们需要跟入看一下 看到这里大家应该就明白了,Shiro为什么会有反序列化漏洞,以及rememberme所传递的数据就究竟是什么,其实就是一个序列化后的PrincipalCollection对象,而这个encrypt就是通过AES来加密序列化后的数据,密钥呢?当然就是硬编码在AbstractRememberMeManager类中的这段base64编码后的字符串了,如下图所示 最后的最后,Shiro会将这段加密后的数据base64编码一遍,然后放入Cookie中,至此Shiro生成rememberme的过程就结束了 那么知道了rememberme是怎么加密生成的,那么自然也就可以很轻易的解密了,尤其还是在密钥硬编码在代码中的这种情况,下面是解密的demo 那么反序列化漏洞产生的点在哪里呢?当用户在登录时勾选了rememberme的时候,Shiro会返回一个rememberme通过Cookie字段传递,然后存储在浏览器中,正常情况下当用户关闭浏览器或者手动删除浏览器中存储的SesseionID的时候,与服务端的当前会话就结束了,当下次打开浏览器再此访问服务端的时候,就需要重新登录。而勾选了rememberme的用户登录后,关闭浏览器后,会话同样会关闭,但是下次打开浏览器请求访问服务端的时候,Cookie中会携带Rememberme来进行请求,从而达到无需登录的效果。 漏洞的产生关键就在于再重新建立会话的这个过程,所以想要触发rememberme的话请求包中就不能有SessionID,删除SessionID后再携带rememberme进行一次请求就可以触发rememberme的反序列化点,如下图所示 最终通过base64解码,然后AES解密,解密后的结果再经过反序列化,就还原成了一个PrincipalCollection对象,至此Shirorememberme的生成以及作用,还有如何触发rememberme反序列化,都已讲解完毕。 总结 ApacheShiro是一款相当优秀的认证授权框架,虽然在护网等大型攻防项目中,经常被作为突破口,但是仍然瑕不掩瑜,shiro的反序列化在流量识别中是比较容易判断的,因为序列化数据的传递必须要通过Cookie中的rememberme字段,但是纵使识别出来,但是如果不知道密钥的话,也无法得知传递的内容。