Apache Shiro 权限绕过漏洞(CVE-2020-13933)

前言

之前分析了 Apache Shiro 权限绕过漏洞(CVE-2020-11989),过了一段时间又出现了新的权限绕过漏洞 (CVE-2020-13933)。应该是在修复的基础上进行了绕过。 CVE-2020-11989 的影响版本为 Apache Shiro < 1.5.3 , CVE-2020-13933 的影响版本为 Apache Shiro < 1.6.0 。


在这个地方 Shiro 对 url 的处理是造成 CVE-2020-11989 的原因之一,Apache Shiro 1.5.3 对此进行的修复。

然后我们查看最新版的 Apache Shiro 1.6.0 发现在 web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java 增加了

从全局上对分号,反斜杠和非ASCII字符进行了过滤

环境搭建

还是选择 https://github.com/l3yx/springboot-shiro 项目进行测试
下载完成后修改一下 pom.xmlorg.apache.shiro 所对应的版本号 为 1.5.3 ,同时将 LoginController 中修改为

1
2
3
4
@GetMapping("/admin/{name}")
public String admin(@PathVariable String name) {
return "admin page,hello " + name;
}

原因之后描述,同时为了方便调试,同时在 pom.xml 文件中加入

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>

生成 war 包,部署于Tomcat

修改 \apache-tomcat-8.0.52\bin\catalina.bat 文件

1
2
3
if not "%JPDA_ADDRESS%" == "" goto gotJpdaAddress
set JPDA_ADDRESS=127.0.0.1:5005
:gotJpdaAddress

catalina.bat jpda start 启动 ,配置 idea 中的远程调试

漏洞分析

Shiro 层

Tomcat 类中的 org.apache.catalina.core.ApplicationFilterChain 是用于管理针对请求 request 的过滤器。

Tomcat 的类 ApplicationFilterChain 是一个 Java Servlet API规范 javax.servlet.FilterChain 的实现,用于管理某个请求 request 的一组过滤器 Filter 的执行。当针对一个 reques 所定义的一组过滤器 Filter 处理完该请求后,组后一个 doFilter() 调用才会执行目标 Servlet 的方法 service(),然后响应对象 response 会按照相反的顺序依次被这些Filter处理,最终到达客户端。

根据调试时显示出的调用链,可以看到先执行到了 shiro 中的 OncePerRequestFilter 这个类

在 shiro 中 org.springframework.web.filter.OncePerRequestFilter 这个类是其他的所有 filter 的父类,所有的 filter 的 doFilter 方法都是调用的这个类中的 doFIlter 方法。

首先调用 getAlreadyFilteredAttributeName() 为过滤器标记,然后判断过滤器是否已经调用过,是否未为当前请求启用。
org.apache.shiro.web.servlet.OncePerRequestFilter#doFilter

然后调用 doFilterInternal 方法,跟进后可以看到执行的是 public abstract class AbstractShiroFilter extends OncePerRequestFilter 中的 doFilterInternal 方法
org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal

跟进函数 executeChain
org.apache.shiro.web.servlet.AbstractShiroFilter#executeChain

跟进函数 getExecutionChain
org.apache.shiro.web.servlet.AbstractShiroFilter#getExecutionChain

跟进其中的 getChain
org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain

跟进方法 getPathWithinApplication
org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getPathWithinApplication

WebUtils#getPathWithinApplication 修复了之前 shiro 1.5.2 所存在的 url 双编码绕过问题。但是我们可以注意到最后的返回值是 /admin/

org.apache.shiro.web.util.WebUtils#getPathWithinApplication

org.apache.shiro.web.util.WebUtils#getServletPath 返回值为 /admin/;whippet

org.apache.shiro.web.util.WebUtils#getPathInfo 返回 ""

org.apache.shiro.web.util.WebUtils#removeSemicolon; 及其之后的全部删除

回到函数 org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain 继续向下执行
如果请求的不是 /,就去除末尾的 / ,返回值就是 /admin

private static final String DEFAULT_PATH_SEPARATOR = "/";

接着根据 filterChainManager.getChainNames() 获取的拦截器进行匹配

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#pathMatches

org.apache.shiro.util.AntPathMatcher#matches
org.apache.shiro.util.AntPathMatcher#match
org.apache.shiro.util.AntPathMatcher#doMatch

pattDirs 的最后一位是 * 所以会返回 false

没有匹配到会返回 null

匹配到的话会指向 ProxiedFilterChain

1.路径匹配:pathMatches(pathPattern, requestURI),默认的Fliter逐个与请求URI进行匹配;2、代理FilterChain:ProxiedFilterChain。如果匹配不上,那么直接走servlet的FilterChain,否则先走shiro的代理FilterChain(ProxiedFilterChain),之后再走servlet的FilterChain

继续单步执行

最后返回 ApplicationFilterChain 相当于并没有执行 Filter

此时就相当于已经绕过了 shiro 的权限验证,可以直接访问到需要权限目录下的文件,但是有时会返回这样的界面,是因为 Spring 并没有匹配到相对应的页面。

Spring层

chain.doFilter(request, response);

接下来的调用栈如图

Spring 在 Tomcat 中运行时需要提供对 Servlet 规范的支持,因为 Tomcat 时基于 Servlert 规范的 web 容器。 DispatcherServlet 是 Servlet 规范的具体实现。在 web 开发过程中,启动 Tomcat 容器时会根据其 Servlet 规范启动 Spring 实现的 DispatcherServlet ,这样就驱动了 Spring 的运行。

DispatcherServlet 在将请求映射到处理器时,调用了 getHandler
org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler

跟进 getHandlerInternal
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal

通过 getLookupPathForRequest 获取请求的绝对路径
org.springframework.web.util.UrlPathHelper#getLookupPathForRequest(javax.servlet.http.HttpServletRequest)

org.springframework.web.util.UrlPathHelper#getPathWithinServletMapping

跟进函数 getPathWithinApplication
org.springframework.web.util.UrlPathHelper#getPathWithinApplication

跟进函数 getRequestUri
org.springframework.web.util.UrlPathHelper#getRequestUri

decodeAndCleanUriString 对 url 进行了解码处理
org.springframework.web.util.UrlPathHelper#decodeAndCleanUriString

此处存在一个问题,因为利用 ; 就可以直接绕过 shiro 的权限验证,但是为什么在直接使用 ; 时会返回 404 错误,在 spring 中不能找到该页面
org.springframework.web.util.UrlPathHelper#decodeAndCleanUriString

decodeAndCleanUriString 会先将 url; 后面的数据进行分割然后再进行 url 解码
解决了这个小小的问题,又产生了一个大大的疑问,在 Apache Shiro权限绕过漏洞分析(CVE-2020-11989) 一文中师傅所利用的 POC 为 /;/test/admin/page 如果是这样的话,从 ; 进行分割,最后得出来的应该是一直去请求 / 页面,不应返回权限下的页面,这个问题暂且放下,继续向下分析。

然后回到函数 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal

跟进 lookupHandlerMethod
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod

跟进其中的 addMatchingMappings
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#addMatchingMappings

org.springframework.web.servlet.mvc.condition.PatternsRequestCondition#getMatchingCondition

org.springframework.web.servlet.mvc.condition.PatternsRequestCondition#getMatchingPatterns

此时我们可以注意到 /admin/{name}/admin/;whippet 能够匹配成功。 会返回/admin/;whippet 的页面,此时的 name 值为 ;wippet ,如果之前我们并没有修改代码,而是固定的页面的话 访问 /admin/page 自然是与 admin/;page 匹配不上的。

参考文章

CVE-2020-13933: Apache Shiro 权限绕过漏洞分析
shiro源码篇 - shiro的filter,你值得拥有