Apache Shiro CVE-2020-1957&CVE-2020-11989身份认证绕过漏洞分析
0x00 漏洞信息
1 | VUL = ["CVE-2020-1957&CVE-2020-11989"] |
0x01 漏洞复现
环境搭建:
首先搭建漏洞环境。看了网上方法,利用Spring Boot + shiro搭建。
先看了大家都参考的博客 https://segmentfault.com/a/1190000019440231 ,发现自己其实创建Spring项目就看不懂,所以先向郭老师请教了下,远程协助我建了项目,又发了我一篇文章 https://developer.aliyun.com/article/695233 ,总算搞懂怎么创建了,感谢郭老师的耐心:heart:。
创建项目,new-project,进入界面,选择Spring Initializr,这里服务填写阿里的下载更快点 https://start.aliyun.com/ ,点击next
填写、选择项目元数据,next
选择Spring Web依赖,点击next,选择项目目录点击finish即创建完成
等待下载完成,在pom.xml添加Shiro依赖,这里shiro版本我选的1.4.0,只要在1.5.2以下都可以
1 | <dependency> |
创建完成后,发现项目下自动生成三个文件:src/main/java-ShiroApplication.class程序入口;src/main/resources-配置文件-application.properties;src/test/java-ShiroApplicationTests.class测试程序
接下来配置shiro过滤规则
创建Realm,实现认证:
1 | package com.debug.shiro.bean; |
配置shiro:
1 | package com.debug.shiro.config; |
配置LoginController:
1 | package com.debug.shiro.controller; |
可以开始运行
访问 http://localhost:8080/login
至此,环境搭建成功。
漏洞复现:
CVE-2020-1957
访问 http://localhost:8080/admin/index ,可以看到被拦截,需要登录
访问 http://localhost:8080/admin/index/ ,直接进入admin界面,无需登录
CVE-2020-11989
访问 http://localhost:8080/;/admin/index ,绕过登录进入admin界面
0x02 漏洞分析
拦截器基础知识
在debug分析前,先学习下Shiro拦截器的规则: https://www.jianshu.com/p/3671b97aab3d
Shiro拦截器有11种,这里只简单看下anon和authc,anon不需要认证可以直接访问,对应上述的/guest/*和/dologin,authc需要认证才能访问,对应/admin/*。
文章中介绍了两种拦截器配置方法:INI文件文件配置和代码配置,复现例子中用的是代码配置。
拦截器规则通配符如下:
1 | ?:匹配一个字符 |
CVE-2020-1957复现例子中用到的是/admin/*,只匹配了admin下的文件,不会匹配多个路径,可以复现成功,如果是/admin/**则复现失败,有一定的局限性;而CVE-2020-11989无论是/admin/*还是/admin/**都可以复现成功。
CVE-2020-1957分析
根据commit信息 https://github.com/apache/shiro/commit/3708d7907016bf2fa12691dff6ff0def1249b8ce ,对WebUtils#getPathWithinApplication等进行了修复
所以我们在org.apache.shiro.web.util.WebUtils#getPathWithinApplication:48行打断点,开始debug模式,然后访问 http://127.0.0.1:8080/admin/index/ 开始分析
F8执行到String requestUri = getRequestUri(request) 然后继续执行
一直F8直到获取URI,getRequestUri(request) 返回的uri是我们输入的请求:/admin/index/
继续F8进入getPathWithinApplication(HttpServletRequest request),返回/admin/index/
继续F8进入PathMatchingFilterChainResolver#getChain,拿到了刚刚getPathWithinApplication获取的请求URI-/admin/index/,将请求的URI链匹配配置的过滤规则链
执行到while条件时,F7进入pathMatches
pathMatches收到两个参数,pattern和path,一个是设置的规则链,一个是请求的路径,return pathMatcher.matches(pattern, path)我们F7进入pathMatcher.matches
此时进入到了org.apache.shiro.util.AntPathMatcher#matches,继续深入,会调用doMatch
分析doMatch,主要是用来匹配规则链和请求
继续执行,一直while循环到/admin/*和/admin/index/进行比较再来看下。分解路径时,是以”/“来进行分解,/admin/index/分解后的是{“admin”,”index”},这里是和/admin/index分解后的结果一样,继续往下进入循环,第一个字符串相比较都是admin肯定一样,继续循环比较第二个*和index,调用matchStrings方法
F7看下matchStrings,比较*和index。循环比较每个字符,当字符为*,说明规则包含*,退出比较,返回true,匹配成功
继续回到doMatch,循环结束,此时返回pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator),pattern.endsWith(this.pathSeparator) ,规则链/admin/*不是以分隔符/结尾,所以返回!path.endsWith(this.pathSeparator),/admin/index/是以分隔符/结尾,所以为true,最终返回false,没有匹配到规则链。假设本来请求的是/admin/index,不以/结尾,这里最终会返回true,成功匹配到规则链
回到getChain,因为返回结果为false即没有匹配成功,所以while会继续循环,直到迭代完成,返回null。这里假如是请求/admin/index,匹配到了,会执行 filterChainManager.proxy(originalChain, pathPattern),也就会进行代理。但是这里直接返回null没有执行proxy从而绕过了鉴权。
接下来继续分析如何获取到/admin/index的资源
F8到getMatchingPattern调用org.springframework.util.AntPathMatcher#match,然后doMatch
深入分析doMatch,可以看到跟之前类似,都是用/直接分解路径,然后依次比较,由于一个是以/结尾 一个不是,所以最终返回了false
getMatchingPattern(“/admin/index”, “/admin/index/“)返回/admin/index/
getMatchingPatterns返回matches={“/admin/index/“}
继续F8,返回到AbstractHandlerMethodMapping#addMatchingMappings,match=”/admin/index/“,因为match不为NULL,认为匹配成功了,直接获取/admin/index的资源
最后梳理下,首先鉴权的时候,匹配规则链doMatch用/进行分隔,/admin/index/和/admin/index效果一样,因为一个以/结尾,一个不以/结尾返回false绕过了鉴权;请求资源的时候,匹配规则链时同样存在问题,getMatchingPattern返回的match不为NULL就判断匹配成功,获取到/admin/index的资源。
CVE-2020-11989分析
更上面同样的断点,访问 http://127.0.0.1:8080/;/admin/index ,开始debug
调用getRequestUri(request),进入该方法
获取返回值时,调用decodeAndCleanUriString(request, uri),分析该方法如图,该方法对含有;的URI进行了处理,当有;时,获取;前的字符串,此时返回/
经过normaliz返回/
此时请求URI由”/;/admin/index”变成了”/“,然后再去匹配chain规则链就跟之前一样,不能匹配从而绕过鉴权
接下来分析是如何获取/admin/index的资源
F8直到org.springframework.web.util.UrlPathHelper#getRequestUri
分析 this.decodeAndCleanUriString
调用removeSemicolonContent,返回//admin/index
继续执行,调用 this.getSanitizedPath
经过getSanitizedPath,将//处理成/,最终获取的是/admin/index
总结下,首先请求/;/admin/index要先进行鉴权,经过org.apache.shiro.web.util.WebUtils#decodeAndCleanUriString(request, uri)处理成/,绕过了授权,然后请求时再经过org.springframework.web.util.UrlPathHelper#decodeAndCleanUriString获取/admin/index的资源
参考链接:
Spring Boot 2.x基础教程:快速入门
Spring Boot 整合 Shiro ,两种方式全总结!
SpringBoot:集成Shiro之拦截器配置
Shiro权限绕过漏洞分析(CVE-2020-1957)
Apache Shiro权限绕过漏洞分析(CVE-2020-11989)