Shiro安全(四):Shiro权限绕过之Shiro-782
0x00 前言
刚好在学shiro安全,就看了看shiro权限绕过方面的洞
0x01 前置知识
学习一下shiro的配置与使用
https://cloud.tencent.com/developer/article/1643122
0x02 漏洞环境
影响版本:shiro < 1.5.3
添加shiro配置
Shiro 中的匹配规则是通过 AntPathMatcher 来进行实现的
? 匹配一个字符
* 匹配一个或多个字符
** 匹配一个或多个目录
ps:这里的规则是 /admin/* 所以 Shiro 并不会对多个目录进行权限校验,例如:/admin/aaa/bbb 这种是不会对其进行权限校验的
添加一个controller,这个只有在认证后才能访问
0x03 漏洞利用
正常情况访问/admin/aaa,因为没有认证所以会弹回登录页
payload:/admin/Hello%252faaaa
成功绕过shiro认证,进入到controller中
0x04 漏洞原理
经过调试发现本质其实就是shiro和spring在处理url时候的规则不同导致的认证绕过
通过断点可以看到请求先经过shiro然后再进入spring分发请求到controller
Shiro层面绕过
Shiro的认证是通过新建filter来实现的,shiro首先会根据uri来获取对应的shiro-filter
跟进getRequestUri,发现uri是通过valueOrEmpty(request.getContextPath()) + “/” + valueOrEmpty(request.getServletPath()) + valueOrEmpty(request.getPathInfo())获得的,而其中
对于request.getServletPath()
,request.getContextPath()
以及 request.getPathInfo()
,以 Tomcat 为例的中间件是会对其进行 URL 解码操作的
此时uri为//admin/Hello%2faaaa
然后再进入decodeAndCleanUriString函数进行一次解码,他这里会判断是否存在封号,如果存在就截取封号前面的,这样是为了避免http://www.xxxx.com/xxxx;jession=xxxxxx这种情况
所以总的来说在getRequestUri函数中,进行了两次url解码操作
返回到最开始的位置,可以看到requestUri为/admin/Hello/aaaa
继续往上层return,来到getChain方法,此时获取到了requestURI,下面就是匹配对应的shiro-filter了。首先获取所有shiro-filter
可以说是一个都匹配不上,最终返回null
return到上层,因为返回为null,所以进入了else,最终只返回了默认的 ApplicationFilterChain,在默认的 ApplicationFilterChain 中是没有任何权限校验
没有一个shiro-filter被匹配到,至此 Shiro 层面的权限就成功绕过了
Spring层面绕过
接下来就是要能让spring将我们的请求发送到对应的controller
熟悉 Spring 的师傅应该都知道在 Spring 中 DispatcherServlet 是负责请求派发的,即将对应的请求转发到对应的 Controller 来处理,其一个主要的操作是通过 HandlerMappings来将请求映射到处理器Handler。在处理过程中会调用 getHandler 方法来获取一个可以处理该请求的Handler(即与该请求匹配的handler,这个handler最终会调用相应controller)
HandlerMapping在这个SpringMVC体系结构中有着举足轻重的地位,充当着url和Controller之间映射关系配置的角色
http://www.51gjie.com/javaweb/921.html
继续往下debug,来到spring获取Handler的地方
在该方法中会获取所有的handlerMappings,通过调用HandlerMapping的getHandler方法来进行判断是否这个Handler可以处理当前请求
继续跟进
继续跟进来到getHandlerInternal,调用了父类getHandlerInternal,这里获取request的uri,这里也是导致绕过最重要的地方,跟进去看看
跟进resolveAndCacheLookupPath
到了spring真正获取uri的地方了
调用了getPathWithinApplication,调用了getRequestUri
注意看,这个getRequestUri与shiro中那个getRequestUri不一样,这个是调用了request.getRequestURI()
这个request.getRequestURI()是不会对uri进行解码的,以前的 shiro 使用 request.getRequestURI()
获取用户请求路径,并自行处理,此时 shiro 默认Servlet 容器(中间件)不会对路径进行 URL 解码操作,通过其注释可以看到;
但是呢后来在 1.5.2 版本的 shiro 更新中,为了修复 CVE-2020-1957 ,将 request.getRequestURI()
置换为了 valueOrEmpty(request.getContextPath()) + "/" + valueOrEmpty(request.getServletPath()) + valueOrEmpty(request.getPathInfo());
而对于 request.getContextPath()
以及 request.getPathInfo()
,以 Tomcat 为例的中间件是会对其进行 URL 解码操作的,此时 shiro 再进行 decodeAndCleanUriString
,就相当于进行了两次的 URL 解码,而与之后的 Spring 的相关处理产生了差异。
下图也可以看到并没有对uri解码
此时再传进decodeAndCleanUriString,相当于是只进行了一次解码
回到上一层,可以看到只进行了一次解码
重新回到 getHandlerInternal 函数,可以看到 lookupPath 和 request 传入了 lookupHandlerMethod 函数中,这里的 lookupPath 其实就是获取到了我们请求对应的 uri的一次解码,接下来就可以根据lookupPath来匹配Controller的Handler了
跟进添加匹配的mapping
可以看到第一个mapping为/unauth这个,如果request能和这个mapping符合就匹配上这个mapping,显然是不符合的
跟进getMatchingMapping(判断是否符合)最终来到getMatchingCondition,进行了一系列的判断来到路由匹配
可以看一下路由/unauth,显然和request中的不匹配
所以返回null
然后一个个mapping和request匹配,最终来到/admin/{name}这个mapping
跟进getMatchingMapping
最终来到路由匹配
spring只对uri一次解码,所以说uri是/admin/Hello%2faaaa,可以和/admin/{name}匹配上
匹配成功就添加到 matches 中
上面的这些功能都是为了在所有匹配的Handler之后需要挑选一个最合适的Handler进行请求的处理,获取到合适的 Handler 之后就进行Handler的访问来处理请求
0x05 总结
总的来说就是shiro和spring对于url的处理方式不同导致的
shiro在获取uri时(为了下一步匹配shiro-filter)对其进行了两次解码
spring在获取uri时(为了下一步匹配controller)对其进行了一次解码
所以说/admin/Hello%252faaaa在shiro眼里就是/admin/Hello/aaaa,即并不会触发shiro的认证filter
而在后面的spring眼里是/admin/Hello%2faaaa,刚好匹配上了/admin/{name}这个controller,从而实现了认证绕过
0x06 参考文章
https://www.yuque.com/tianxiadamutou/zcfd4v/emcdeq
https://su18.org/post/shiro-3/#cve-2020-11989
https://cloud.tencent.com/developer/article/1643122
https://www.anquanke.com/post/id/218270#h3-7
http://www.51gjie.com/javaweb/921.html