0x00 前言
在看 java web 审计的文章发现在其中有介绍关于 Spring 与 Shiro 之间权限绕过的问题,正好之前没有学习过,所以趁着机会学习一下
0x01 漏洞环境
这里可以在之前 Shiro 内存马注入环境的基础上进行一些简单修改
链接:https://github.com/KpLi0rn/ShiroVulnEnv
首先将 pom.xml 中对 shiro 的版本进行修改
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.2</version>
</dependency>
0x02 前置知识
下图红框处是 Shiro 的 URL 匹配规则
匹配规则
Shiro 中的匹配规则是通过 AntPathMatcher 来进行实现的
? 匹配一个字符
* 匹配一个或多个字符
** 匹配一个或多个目录
0x01 CVE-2020-11989
漏洞产生的原因是因为 Spring 与 Shiro 之间对 url 的处理不同从而导致权限绕过
利用条件
- Apache Shiro <= 1.5.2
- Spring 框架中只使用 Shiro 鉴权
- 需要后端特定的格式才可进行触发
- 即:Shiro权限配置必须为 /xxxx/* ,同时后端逻辑必须是 /xxx/{variable} 且 variable 的类型必须是 String
漏洞环境
在上文的环境基础下,在 Shiro 包下的 ShiroConfig 中,添加红框处代码
map.put("/admin/*","authc");
即当我们访问 /admin/xxxx 的路径的时候 Shiro 会对其进行权限校验
ps:这里的规则是 /admin/*
所以 Shiro 并不会对多个目录进行权限校验,例如:/admin/aaa/bbb
这种是不会对其进行权限校验的
然后在 UserController 中添加如下代码
这里一定要 /xxxx/{} 的形式,且参数为 String
@ResponseBody
@GetMapping("/admin/{name}")
public String namePage(@PathVariable String name){
return "Hello" + name;
}
漏洞演示
正常访问 /admin/demo 的时候,由于 Shiro 的权限校验,从而会跳转到 /login 处
当我们访问 /admin/Hello%252fBKpLi0rn 时候发现绕过了权限校验
从访问的路由可以很容易的看出来主要是因为 %252f ,也就是两次 URL 解码之后的 /
漏洞分析
通过打断点发现我们的请求会先经过 Shiro 然后再到 Spring中
Shiro层
在 GetMapping 处打断点,开始 debug
通过上面断点的位置,我们可以直接定位到 Shiro 处理请求的位置,WebUtils#getPathWithinApplication
在 getRequestUri 函数中会对我们的 uri 进行处理,跟进该函数
发现如果不为空的话就会将我们的 uri 传入 decodeAndCleanUriString 函数
ps: 中间件收到我们的 get 请求会先进行一次url解码,所以这里 Shiro 收到的是 Hello%2fBLi0rn
来到 decodeAndCleanUriString 函数,在该函数中将 uri 传入了decodeRequestString 函数中进行 uri 解码,跟进该函数
在 decodeRequestString 函数中会对 uri 进行一次 URL 解码
将解码之后的 uri 进行赋值,然后会判断其中是否含有分号,如果有的话就截取分号前的内容进行返回
这么做应该主要是为了应对如下这种情况:
http://www.xxxx.com/xxxx;jession=xxxxxx
然后将处理好的 uri 传递给了 normalize 函数
在 normalize 函数中会对 uri 进行一些处理
将 '\\' => '/'
将 '/./ => '/'
将 /../
前面的内容和后面进行拼接
处理完之后进行返回
将返回赋到 requestUri
继续跟下去知道来到 PathMatchingFilterChainResolver#getChain 中,在该函数中会获取我们在 ShiroConfig 中的规则,调用 pathMatches 函数来进行匹配,跟进 pathMatches 函数
在 pathMatches 函数中可发现匹配是通过 AntPathMatcher 来实现的 ,跟进 matches 方法
最终调用了 AntPathMatcher#doMatch 方法
在 doMatch 中实现匹配
这里我们的规则是 /admin/*
但是我们此时的 path 为 /admin/Hello/Bli0rn
由于没有匹配成功,所以返回 false
最后回到 getChain 函数,由于规则都遍历了没有发现匹配的,就返回 null,至此 Shiro 的权限就绕过了
由于 getChain 中返回的是 null,所以这里的 resolved 也是 null
由于 resolved 为 null,只会返回默认的 ApplicationFilterChain,在默认的 ApplicationFilterChain 中是没有任何权限校验
至此 Shiro 层面的权限就成功绕过了
题外话
如果是正常的拦截情况的话,会返回 ProxiedFilterChain,即先走 Shiro 自身的 Filter,然后再委托给 Servlet 容器的 FilterChain 进行 Servlet 容器级别的 Filter 链执行
Spring 层
文章链接:http://www.51gjie.com/javaweb/921.html,https://www.anquanke.com/post/id/218270#h3-7
熟悉 Spring 的师傅应该都知道在 Spring 中 DispatcherServlet 是负责请求派发的,即将将对应的请求转发到对应的 Controller 来处理,其一个主要的作用是通过 HandlerMapping 将请求映射到处理器。在处理过程中会调用 getHandler 方法来获取一个可以处理该请求的Handler
org.springframework.web.servlet.DispatcherServlet#doDispatch 大约484行左右 调用了 getHandler 方法
在本函数内会遍历所有已加载的 handlerMappings ,通过调用HandlerMapping的getHandler方法来进行判断是否这个Handler可以处理当前请求
发现在 getHandler 中调用了 getHandlerInternal 函数,跟进该函数
在 getHandlerInternal 中,调用了 getUrlPathHelper().getLookupPathForRequest(request) 该方法会根据请求解析出具体的用于匹配Handler的url,这是一个很关键的步骤,寻找合适的Handler就是根据url来进行的
在 getLookupPathForRequest 函数中调用了 getPathWithinServletMapping 然后赋给了 rest ,如果 rest 为 ""
那么就调用 getPathWithinApplication 来根据我们传入的 request 获取 应用内的路径
getPathWithinServletMapping:返回给定请求的Servlet映射中的路径,即请求 url 中超出调用 Servlet 的部分,在官方文档中给出了 demo
跟进 getPathWithinApplication 函数,发现会调用 getRequestUri 来获取我们的 requestUri ,跟进该函数
发现首先会从上下文的 javax.servlet.include.request_uri
属性中获取,如果为 null 则调用 request.getRequestURI() 获取到我们的 uri,然后通过 decodeAndCleanUriString 进行了一次 url 解码
从 /admin/Hello%252fBKpLi0rn
变为了 /admin/Hello%2fBKpLi0rn
重新回到 getHandlerInternal 函数,可以看到 lookupPath 和 request 传入了 lookupHandlerMethod 函数中
这里的 lookupPath 其实就是获取到了我们请求对应的 uri,接下来就可以根据lookupPath来匹配Controller的Handler了
Spring 获取路径映射
参考文章:https://www.jianshu.com/p/1136212b9197
matches 会存储所有匹配到的方法,如果matches为空就进入该判断,调用 addMatchingMappings 函数来添加匹配的Handler
继续跟进 getMatchingMapping 函数
进行了一系列的判断,跟进 getMatchingCondition
到 getMatchingCondition 函数,可以看到 lookupPath 传入了 getMatchingPatterns 函数,跟进该函数
getMatchingPatterns 函数中会将我们的请求和配置的 url 进行比较,匹配成功就添加到 matches 中
上面的这些功能都是为了在所有匹配的Handler之后需要挑选一个最合适的Handler进行请求的处理,获取到合适的 Handler 之后就进行Handler的访问来处理请求了(后面就不跟了 Orz
最终
漏洞修复
在 Shiro 1.5.3 版本中对 getPathWithinApplication 进行了修改,取消了 url 解码的函数,所以我们这里的 uri 并不会被完全解码
0x02 CVE-2020-1957
其实应该是这个写在最前面的,但是发现的时候 11989 已经已经写完了 Orz
利用条件
- Apache Shiro <= 1.5.1
- Spring 框架中只使用 Shiro 鉴权
漏洞环境
修改 pom.xml 中的版本为 2.1.5.RELEASE
ps:之前的 2.4.5 复现失败了,会报404,换成 2.1.5 就可以了
修改 shiro 版本为 1.5.1
在 UserController 中添加如下代码
@ResponseBody
@RequestMapping(value = "/admin/index", method = RequestMethod.GET)
public String admin() {
return "admin secret bypass and unauthorized access";
}
@ResponseBody
@RequestMapping(value = "/demo", method = RequestMethod.GET)
public String demo() {
return "demo";
}
在 ShiroConfig 中变为如下规则
map.put("/doLogin", "anon");
map.put("/demo/**","anon");
map.put("/unauth", "user");
map.put("/admin/**","authc");
map.put("/**", "authc");
漏洞演示
在本实验 demo 中访问 /demo 是不需要权限的,但是访问 /admin/index 时会被 Shiro 进行验权从而跳转到 /login
但是通过 /demo/..;/admin/index 就可以绕过 shiro 的权限来访问到 /admin/index
可以看到成功绕过 shiro 进行权限校验
漏洞分析
在 getPathWithinApp 中调用了 getRequestUri 中获取我们请求的 uri
在 getRequestUri 中会调用 decodeAndCleanUriString
在 decodeAndCleanUriString 处,会获取 uri 中分号的索引,如果 uri 中存在分号那么就会截取分号前的字符串
后面会来到 PathMatchingFilterChainResolver#getChain 中进行权限匹配,此时我们的 requestURI 为 demo/.. 由于我们 Shiro 的规则为 /demo/** anon
,因此校验通过
所以 Shiro 这部分的绕过其实就是因为截取了分号前面,也就是我们这里的 /demo/.. 然后和 /demo/ 匹配上了,由于我们的 /demo/ 是没有任何权限限制的,因此就绕过了
在实际情况中应该 login 多一些,例如 /login/..;/admin/index
这样 Shiro 部分的权限就绕过了
后面就是 Spring 的部分
和上文一样我们来到了 getHandler 函数处
跟进 getHandlerInternal 函数
在 getHandlerInternal 函数中会调用 getLookupPathForRequest 来根据我们的请求返回对应的 uri
在 getLookupPathForRequest 函数中调用了 getPathWithinServletMapping 来获取请求的相对路径,跟进该方法
getPathWithinServletMapping:返回给定请求的Servlet映射中的路径,即请求 url 中超出调用 Servlet 的部分,在官方文档中给出了 demo
在 getPathWithinApplication 函数中调用了 getServletPath,获取请求对应的 Servlet 的路径
其实该方法中就是具体的对请求的url的处理
在 getServletPath 函数中,首先会从上下文中进行获取,如果获取结果为 null 就会调用 request.getServletPath 即返回请求的URL中调用Servlet的部分
我们这里跟进一下 getServletPath 函数,继续跟进
可以看到返回了 this.mappingData.wrapperPath 也就是 /admin/index (即 Tomcat 中 servlet-path 匹配后的结果)
http://dengchengchao.com/?p=1065
最后返回给 springboot
漏洞修复
将原先的 request.getRequestURI() 替换成了 getContextPath() 、getServletPath() 、getPathInfo() 的组合,这样就能获取我们想要的了,从而避免因为获取差异性而导致绕过,这样就与返回给 springboot 的路径保持一致了
0x03 CVE-2020-13933
利用条件
- Apache Shiro < 1.6.0
- Spring 框架中只使用 Shiro 鉴权
-
需要后端特定的格式才可进行触发
- 即:Shiro权限配置必须为 /xxxx/* ,同时后端逻辑必须是 /xxx/{variable} 且 variable 的类型必须是 String
漏洞环境
同 CVE-2020-11989 环境,只要将版本改为 1.5.3 即可,修改 pom.xml
漏洞演示
/admin/%3BKpLi0rn
漏洞分析
在 1.5.3 版本之后,Shiro 不会进行 url 的二次解码,但是在 removeSemicolon 中仍存在绕过的可能性
跟进该函数
removeSemicolon 函数中,会获取第一次出现分号的索引,然后截取分号前的 uri
这种情况的处理应该是为了应对 www.xxxx.com/admin;jession=asfoasdo
这种情况
所以我们只要利用 /admin/;whatever 这样的结构就可以绕过 Shiro 的权限校验
红框处会将 uri 最后的 / 进行去除,所以此时的 requestURI 为 /admin 自然是不符合我们这里的 shiro 拦截规则的 /admin/*
然后在 Spring 中则是会将后面部分当作是参数进行获取从而输出
漏洞修复
https://github.com/apache/shiro/commit/dc194fc977ab6cfbf3c1ecb085e2bac5db14af6d
增加了 InvalidRequestFilter 类来对一些特殊情况进行处理
遇到特殊字符会直接报错
同时增加了 /** 的规则,来防止一些匹配不到的情况
0x04 CVE-2020-17523
利用条件
-
Apache Shiro < 1.7.1
-
Spring 框架中只使用 Shiro 鉴权
-
需要后端特定的格式才可进行触发
- 即:Shiro权限配置必须为 /xxxx/* ,同时后端逻辑必须是 /xxx/{variable} 且 variable 的类型必须是 String
漏洞环境
同 CVE-2020-11989 环境,pom.xml 中版本修改为 1.7.0 或及以下即可
漏洞演示
漏洞分析
该绕过发生在 AntPathMatcher#doMatch 中,其中利用了 tokenizeToStringArray 函数分割传入的 uri 为数值,跟进该函数
在该函数中会调用 trim 将我们的空格给去掉,从而导致 /admin/ 与 /admin/* 不匹配,因此绕过 shiro 权限校验
漏洞修复
增加了选项,tokenizeToStringArray 函数中默认不会进行 trim
结合上图传入的微 false,因此默认情况下 token = token.trim() 并不会被执行
0x05 总结
整体学习下来其实根本问题就是 Shiro 与 Spring 对请求处理不同从而导致我们可以构造特殊的 uri 从而绕过 Shiro 的权限校验
0x06 参考链接
https://hpdoger.cn/2021/02/08/title:%20Shiro%E6%9D%83%E9%99%90%E7%BB%95%E8%BF%87%E6%B1%87%E6%80%BB/
https://www.anquanke.com/post/id/218270#h3-7
https://www.jianshu.com/p/1136212b9197