shiro从入门到权限绕过漏洞 | 宜武汇-ag真人国际厅网站

之前学习的时候一直没有写笔记,这次是来把shiro的笔记全部补回来的。之前写过一个shiro从0到1,但是感觉还是总结的很少。

这一次从零开始。

shiro的简介

shiro现在是比较火的一个安全框架了,还有很多安全框架,比如spring security等等,说是安全框架,也可以说成是权限管理框架。

权限管理可以实现对用户的访问系统的控制,比如说/admin/userlist,这个路由访问的是用户管理的页面,如果没有权限控制的话,如果谁都可以访问的话,那不就成了未授权访问了。
shiro的组成(核心架构)


以上这张图是shiroag真人国际厅网站官网的一个架构图,接下来一一介绍这几个是什么意思:

subject

subject不难理解,他其实就是一个主体,也可以说是当前应用程序,又可以说是当前用户。总的来说subject代表当前用户或者当前程序。

在shiro中subject是一个接口,他定义了很多认证授权的方法。

什么是认证? 认证就是判断你这个用户是不是合法用户,他是一个过程,可以理解为是一个认证的过程。

什么是授权?授权其实就是你认证成功之后,你的权限能访问系统的那些资源,当我们身份认证通过后需要分配权限决定你可以访问那些资源。

securitymanage

securitymanage从名字我们可以看出它是安全管理器,当我们的subject去认证的时候,需要通过securitymanage安全管理器来负责认证和授权,可以理解为securitymanage安全管理器就是干认证和授权这些事情的。而securitymanage安全管理器又要通过authenticator认证器进行认证,通过authorizer授权器进行授权,通过sessionmanag会话管理器进行会话管理,有没有发现他就相当于一个中介,他来接收这些事情,而干这些事情的不是他来做的,而是后面这些什么会话管理器,授权器这些来做的。

authenticator

authenticator即为认证器,我们上面也说了securitymanage安全管理器中途转发过来,然后由我们的认证器来进行身份认证。但是我们认证的数据从哪来?那就用到了realm,realm从数据库中去获取到用户信息,然后认证器来做身份认证。

authorizer

authorizer即为我们的授权器,那我们通过认证器认证权限之后,我们是不是得通过授权器来判断这个用户身份有什么权限,他可以访问那些资源。

realm

realm他是一个领域,其实相当于数据源,比如我们在身份认证的时候我们是不是得调用认证器,通过认证器,我们需要从realm中获取到用户的数据,比如用户的数据在mysql数据库,那么realm就需要从mysql数据库中去获取到用户的信息,然后来做身份认证。可以理解realm相当于一个数据库。但是在realm中也有一些认证授权相关的操作。

sessionmanager

sessionmanager是一个会话管理器,,shiro框架定义了一套会话管理, 它不依赖web容器的session,所以shiro可以使用在非web 应用上,也可以将分布式应用的会话集中在一点管理,此 特性可使它实现单点登录。

sessiondao

sessiondao其实就是会话,比如要将session存储到数据库,那么可以通过jdbc来存储到数据库。

shiro中的认证

身份信息

principal身份信息,你可以理解为是一个用户名,或者邮箱,具有标识类的信息。

凭据信息

credential凭据信息,你可以理解为是一个密码或者证书。

认证流程

通过我们前面的理解,shiro基本的认证流程就是:

当我们的用户去认证的时候,用户携带我们的身份信息,凭据信息,也就是我们的用户名和密码,shiro会将我们的用户名和密码封装成一个token,然后通过安全管理器,安全管理器去调用认证器,认证器去调用我们的realm去获取数据,然后进行比对,如果对比成功的话,那么就认证成功了,否则认证失败。

认证举例
引入依赖
 org.apache.shiro shiro-core 1.5.3  
shiro配置文件

这个配置文件代表的是你的用户名或者密码,到时候如果和其他框架整合的话需要创建shiroconfig。这里测试的话就直接将用户名和密码写死了。这个文件创建在resource目录下。名称为shiro.ini

[users] relaysec=123456
测试代码:
package com.powernode; import org.apache.shiro.securityutils; import org.apache.shiro.authc.incorrectcredentialsexception; import org.apache.shiro.authc.unknownaccountexception; import org.apache.shiro.authc.usernamepasswordtoken; import org.apache.shiro.mgt.defaultsecuritymanager; import org.apache.shiro.realm.text.inirealm; import org.apache.shiro.subject.subject; public class shirodemo { public static void main(string[] args) { //1.创建安全管理器对象 defaultsecuritymanager securitymanager = new defaultsecuritymanager(); //2.给安全管理器设置realm securitymanager.setrealm(new inirealm("classpath:shiro.ini")); //3.securityutils 给全局安全工具类设置安全管理器 securityutils.setsecuritymanager(securitymanager); //4.关键对象 subject 主体 subject subject = securityutils.getsubject(); //5.创建令牌 usernamepasswordtoken token = new usernamepasswordtoken("relaysec","123456"); try{ subject.login(token);//用户认证 system.out.println("登录成功"); }catch (unknownaccountexception e){ e.printstacktrace(); system.out.println("认证失败: 用户名不存在~"); }catch (incorrectcredentialsexception e){ e.printstacktrace(); system.out.println("认证失败: 密码错误~"); } } } 

shiro认证源码分析

用户名认证

我们在login这里下断点,跟进去。虽然我们调用的是subject的login方法。但是可以看到他实际调用的是我们安全管理器的login方法,这里传进去两个值,第一个this就是我们的自身类,第二个值的话就是我们的token,我们这个token里面包含着我们的用户名和密码,在上面测试程序可以看到,在new usernamepasswordtoken将我们的用户名和密码传递了进去。所以我们跟进去login方法。


来到login方法,发现调用到了defaultsecuritymanager的login方法,就是我们上面new的安全管理器,在这里调用authenticate方法,我们跟进去。


来到authenticate方法,发现他调用的是authenticator的authenticate方法,authenticator是不是我们的认证器啊,所以他调用了我们认证的authenticate方法,我们跟进去。


来到authenticate方法,首先判断我们的token是否为null,如果为null的话就会抛出异常。

然后调用doauthenticate方法,可以发现如果返回的信息info为null的话,他就会抛出异常,我们跟进去doauthenticate方法。


来到doauthenticate方法,调用getrealms方法,拿到我们所有的域,里面包含我们的用户名和密码,接着进行if判断,判断我们的realm的size是否等1,我们进入if,接着调用dosinglerealmauthentication方法。


来到dosinglerealmauthentication方法,首先进行判断我们的realm是否支持token,然后调用我们realm的getauthenticationinfo方法,我们跟进去。


来到getauthenticationinfo方法,首先调用getcachedauthenticationinfo方法,从缓存中拿我们的信息,我们是没有配置缓存管理器的,第一次访问是没有缓存的,所以我们进入if,调用dogetauthenticationinfo方法,跟进去。


来到dogetauthenticationinfo方法,这里首先将我们的token强制转换为usernamepasswordtoken,然后调用getuser方法,根据我们传入的token中的用户名调用getuser去获取用户。我们跟进去。


来到getuser方法,此时我们的username就是我们的传入的用户名。最后通过get方法获取到我们的用户名。


回到dogetauthenticationinfo方法,可以看到此时返回的account就是我们的用户名。然后进行判断如果account不为空的话,进入到if,然后判断我们的account是否加锁了,然后调用iscredentialsexpired方法判断你的密码是否过期。我们没有加锁也没有做过期的处理,所以到这里用户名的处理就结束了。我们可以发现真正用户名的处理是在simpleaccountrealm的dogetauthenticationinfo方法中实现的。最后返回account,我们返回上一个方法。

密码认证

返回到getauthenticationinfo方法,这里首先判断我们的token不等于并且返回的info不等于null的话,调用cacheauthenticationinfoifpossible方法,默认会给我们加一个缓存。然后我们继续往下走。


继续判断info如果不等于null的话,他会调用assertcredentialsmatch方法,判断我们token中的密码和info中的密码是否一致,我们跟进去。


来到assertcredentialsmatch方法。

首先获取我们的密码匹配器,然后判断我们的密码匹配器如果不等于null的话,调用docredentialsmatch方法进行密码匹配,跟进去。


来到docredentialsmatch方法,最后通过equals进行比对,这是默认的密码匹配器。如果我们的密码加密过,加盐过,他还会有其他的操作。

shiro中的授权

授权

授权上面也说过了,其实就是用户通过认证之后,你拥有那些权限,你可以访问那些资源。对于没有权限访问的资源是无法访问的。

授权流程

当我们的用户去认证的时候,用户携带我们的身份信息,凭据信息,也就是我们的用户名和密码,shiro会将我们的用户名和密码封装成一个token,然后通过安全管理器,安全管理器去调用认证器,认证器去调用我们的realm去获取数据,然后进行比对,如果对比成功的话,那么就认证成功了,否则认证失败。

上面是我们的认证流程,当我们认证成功之后,登入系统之后,判断是否对访问的资源有操作权限,如果有操作权限那么就可以访问,如果没有操作权限,那么就不能访问。

springboot整合shiro

引入依赖

首先创建一个springboot的项目,引入maven依赖:

这里要注意的是我们引入的shiro依赖不能是springboot里面的,要引入单独的。

  xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation="http://maven.apache.org/pom/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0  org.springframework.boot spring-boot-starter-parent 2.7.6    war com.powernode shiro-boot-shiro 0.0.1-snapshot shiro-boot-shiro shiro-boot-shiro  1.8    org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-devtools runtime true   org.apache.tomcat tomcat-juli 8.5.23   org.projectlombok lombok true   org.springframework.boot spring-boot-starter-test test    org.apache.tomcat.embed tomcat-embed-jasper   jstl jstl 1.2     org.apache.shiro shiro-web 1.4.2                             org.apache.shiro shiro-web 1.7.0   org.apache.shiro shiro-spring 1.7.0      org.springframework.boot spring-boot-maven-plugin 2.5.0    org.projectlombok lombok        
创建shiroconfig.java

1.创建shirofilter

shirofilterfactorybean shirofilterfactorybean = new shirofilterfactorybean(); //给filter设置安全管理器 shirofilterfactorybean.setsecuritymanager(defaultwebsecuritymanager); //配置系统受限资源 //配置系统公共资源 map<string,string> map = new hashmap<string,string>(); map.put("/admin/**","anon");//authc 请求这个资源需要认证和授权 map.put("/admin/users","authc"); map.put("/demo/**","anon"); map.put("/index.jsp","authc"); map.put("/hello/*", "authc"); map.put("/tojsonlist/*","authc"); //默认认证界面路径---当认证不通过时跳转 shirofilterfactorybean.setloginurl("/login.jsp"); shirofilterfactorybean.setfilterchaindefinitionmap(map); return shirofilterfactorybean; 

2.创建安全管理器

@bean public defaultwebsecuritymanager getdefaultwebsecuritymanager(realm realm){ defaultwebsecuritymanager defaultwebsecuritymanager = new defaultwebsecuritymanager(); //给安全管理器设置 defaultwebsecuritymanager.setrealm(realm); return defaultwebsecuritymanager; } 

3.创建自定义的realm

//3.创建自定义realm @bean public realm getrealm(){ customerrealm customerrealm = new customerrealm(); return customerrealm; } 

4.自定义的realm

package com.powernode.shirobootshiro.realm; import org.apache.shiro.authc.authenticationexception; import org.apache.shiro.authc.authenticationinfo; import org.apache.shiro.authc.authenticationtoken; import org.apache.shiro.authc.simpleauthenticationinfo; import org.apache.shiro.authz.authorizationinfo; import org.apache.shiro.authz.simpleauthorizationinfo; import org.apache.shiro.realm.authorizingrealm; import org.apache.shiro.subject.principalcollection; import org.springframework.util.collectionutils; import org.springframework.util.objectutils; import java.util.list; //自定义realm public class customerrealm extends authorizingrealm { @override protected authorizationinfo dogetauthorizationinfo(principalcollection principals) { return null; } @override protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception { system.out.println("============="); //从传过来的token获取到的用户名 string principal = (string) token.getprincipal(); system.out.println("用户名"principal); //假设是从数据库获得的 用户名,密码 string password_db="123"; string username_db="zhangsan"; if (username_db.equals(principal)){ // simpleauthenticationinfo simpleauthenticationinfo = return new simpleauthenticationinfo(principal,"123", this.getname()); } return null; } } 

测试:

这是一个测试jsp,我们在shiroconfig文件中配置了那些资源我们可以访问,那些资源我们不能访问,就是这几行代码,这里map的key值代表的是我们的资源,map的value值代表的是我们的权限,authc代表我们是需要认证和授权的,anon代表我们不需要认证和授权,接下来我们再聊shiro的绕过,其实代码审计去审的就是shiroconfig文件,看他的jar包,以及shiroconfig配置文件。

map<string,string> map = new hashmap<string,string>(); map.put("/admin/**","anon");//authc 请求这个资源需要认证和授权 map.put("/admin/users","authc"); map.put("/demo/**","anon"); map.put("/index.jsp","authc"); map.put("/hello/*", "authc"); map.put("/tojsonlist/*","authc"); 

我们这里写了一个登录的controller,登录成功之后转发到index.jsp,否则直接转发到login.jsp文件。

shiro过滤的流程

在pathmatchingfilterchainresolver类中的getchain方法对我们请求的资源进行了过滤,那么是怎么调用到getchain的呢?当一个请求到达tomcat时,tomcat以责任链的形式调用了一系列filter,onceperrequestfilter就是众多filter中的一个。它所实现的dofilter方法调用了自身的抽象方法dofilterinternal。

pathmatchingfilterchainresolver.getchain就是被在dofilterinternal中被一步步调用的调用的。


来到getchain方法,首先调用getfilterchainmanager方法获取过滤器,然后接着调用getpathwithinapplication方法来获取请求路径,我们跟进去。


来到getpathwithinapplication方法,这里又调用了一个工具类webutils的getpathwithinapplication方法,跟进去。


来到getpathwithinapplication方法,这里调用了getservletpath方法,我们跟进去。


来到getservletpath方法,首先在request域中查找 javax.servlet.include.servlet_path,接下来判断如果不等于null的话那么就直接返回,如果等于null的话就调用valueorempty方法。我们这里是查找不到的,所以跟进valueorempty方法。最后返回的就是我们的请求路径。


回到getpathwithinapplication方法,我们返回的值是/admin/users,然后调用getpathinfo,getpathinfo方法返回的值是空的,所以拼接起来还是/admin/users,然后调用normalize方法,然后调用removesemicolon方法,跟进去。


来到removesemicolon方法,这里首先将我们传进去的路径调用indexof方法,如果我们的字符串里面有 ; 号的话,那就返回第一个索引。明显我们是没有的,所以返回-1,接下来判断如果不等于-1的话,直接返回uri,如果等于-1的话,那么就从分号开始截取后面的值然后返回。


跟进normalize方法,继续跟进normalize方法,normailze方法会进行一系列的判断,此时的path就是我们请求的路径,首先判断是否为null,然后把path赋值给了normalized变量,紧接着判断replacebackslash是否为true,他是false,所以我们不用管,继续往下,判断路径里面是否有/. 如果有的话直接返回 / ,下面大家可以自己看下,我这里就不多说了。走完normalize方法之后,我们返回到getchain方法。

private static string normalize(string path, boolean replacebackslash) { if (path == null) return null; // create a place for the normalized path string normalized = path; if (replacebackslash && normalized.indexof('\\') >= 0) normalized = normalized.replace('\\', '/'); if (normalized.equals("/.")) return "/"; // add a leading "/" if necessary if (!normalized.startswith("/")) normalized = "/"  normalized; // resolve occurrences of "//" in the normalized path while (true) { int index = normalized.indexof("//"); if (index < 0) break; normalized = normalized.substring(0, index)  normalized.substring(index  1); } // resolve occurrences of "/./" in the normalized path while (true) { int index = normalized.indexof("/./"); if (index < 0) break; normalized = normalized.substring(0, index)  normalized.substring(index  2); } // resolve occurrences of "/../" in the normalized path while (true) { int index = normalized.indexof("/../"); if (index < 0) break; if (index == 0) return (null); // trying to go outside our context int index2 = normalized.lastindexof('/', index - 1); normalized = normalized.substring(0, index2)  normalized.substring(index  3); } // return the normalized path that we have completed return (normalized); } 

回到getchain方法,首先判断我们的url路径不等于null的话并且default_path_separator,这个其实是一个 “/” ,他判断我们的url里面是否存在 /,然后判断我们的url结尾是不是/ 显现不是的,跳过if。


来到下一个if,首先他通过调用getchainnames方法,获取到我们设置的权限访问的资源路径,然后进行匹配。首先判断是否为null,然后判断里面是否包含,最后判断结尾是否是 / 。跳出if。


来到下一个if,接着继续循环判断。

因为我们在map中的第一个写的就是/admin/users,所以他匹配上了,最后调用proxy代理方法进行处理。

shiro绕过漏洞分析

cve-2020-1957

在复现和分析之前,首先说一下环境的问题,如果你的springboot版本过高的话,那么可能会复现不成功,因为在shiro层面绕过之后,springboot也需要解析路径的,所以如果你的springboot版本过高的话,可能是复现不成功的。并且不能使用springboot集成的shiro吗,那样子也有可能导致复现不成功。

环境:springboot:2.2.6.releas

org.springframework.boot spring-boot-starter-parent 2.2.6.release 

shiro版本:1.5.0

 org.apache.shiro shiro-web 1.5.0   org.apache.shiro shiro-spring 1.5.0  

shiroconfig配置:

linkedhashmap<string, string> map = new linkedhashmap<string, string>(); map.put("/login","anon");//anon 设置为公共资源 放行资源放在下面 // map.put("/user/register","anon");//anon 设置为公共资源 放行资源放在下面 // map.put("/register.jsp","anon");//anon 设置为公共资源 放行资源放在下面 // map.put("/user/getimage","anon"); map.put("/dologin", "anon"); map.put("/demo/**","anon"); map.put("/unauth", "user"); map.put("/admin/*","authc"); //默认认证界面路径---当认证不通过时跳转 shirofilterfactorybean.setloginurl("/login.jsp"); shirofilterfactorybean.setfilterchaindefinitionmap(map); return shirofilterfactorybean; 

controller:

绕过方式: /demo/..;/admin/index


复现:

漏洞分析:

首先我们定位到pathmatchingfilterchainresolver类的getchain方法,这个方法是处理过滤的,前面也说过了。

首先调用getpathwithinapplication方法获取路径,跟进去。


来到getpathwithinapplication方法,继续跟进webutils的getpathwithinapplication方法。

首先getcontextpath方法获取工程路径,调用getrequesturi获取访问路径,跟进去getrequesturi方法,


来到getrequesturi方法,首先从域中获取,获取不到的话,调用getrequesturi方法获取路径,获取的就是我们访问的//demo/..;/admin/users 这个路径,然后调用decodeandcleanuristring方法进行处理。我们跟进去。


来到decodeandcleanuristring方法,通过indexof方法,因为我们的路径中存在分号,所以他获取到的位置是第9个,

然后判断如果不等于-1的话,调用substring方法进行字符串截取,从0到9 包前不包后 ,也就是说分号不需要截取,截取出来的字符串就是//demo/..。然后返回上一个方法。


来到normalize方法,这里进行了字符的替换,

替换反斜线

替换 ///

替换 /.//

替换 /..//

然后返回。


回到getchain方法,首先判断如果url不等于null并且他的最后一位是 / 的话,进行字符串截取然后赋值,我们拿到的字符串路径是/demo/.. 所以往下走。

然后循环遍历我们的map中的内容,就是我们在shiroconfig中写的那些过滤的内容,然后进行一一匹配,最后匹配到/demo/**的时候,然后调用proxy方法,我们跟进去。


来到proxy方法,首先调用getchain方法获取到请求路径对应的过滤器,然后调用过滤器的proxy方法,来到proxy方法。


来到proxy方法,首先创建了一个proxiedfilterchain对象,这个对象是一个代理对象。


基本上到这里我们的原始请求就会进入到 springboot中. springboot对于每一个进入的request请求也会有自己的处理方式,找到自己所对应的controller。

我们定位到spring处理请求的地方。 我们跟进去getpathwithinapplication方法。

org.springframework.web.util.urlpathhelper#getpathwithinservletmapping 


来到getpathwithinapplication方法,调用getcontextpath方法获取到工程路径,调用getrequesturi获取访问路径,我们跟进getrequesturi方法。


来到decodeandcleanuristring方法,跟进removesemicoloncontent方法。


首先获取到分号的位置,然后while循环如果不等于-1的话,然后进行字符串截取,将我们的分号截取掉 然后返回的路径就是//demo..

回到decodeandcleanuristring方法,调用decoderequeststring进行decode解码,然后调用getsanitizedpath方法进行过滤 //

然后返回。


回到getpathwithinapplication方法,可以发现我们的分号已经被去掉了。


到这里基本上的流程就结束了,可以发现在spring中会过滤分号,而在shiro中不会。导致权限绕过。

原文链接:https://xz.aliyun.com/t/12643

网络摘文,本文作者:15h,如若转载,请注明出处:https://www.15cov.cn/2023/08/27/shiro从入门到权限绕过漏洞/

发表评论

邮箱地址不会被公开。 必填项已用*标注

网站地图