今年以来,shiro连续曝出多个身份验证绕过漏洞,在此梳理一下shiro身份验证绕过漏洞史。
SHIRO介绍
很多人认识Shiro是因为其反序列化漏洞的广泛性和严重性。Shiro本身是一个常用的Java安全框架,用于执行身份验证、授权、密码和会话管理功能,有着易用、全面、灵活等特性,shiro被广泛使用。通常Shiro会和Spring等框架一起搭配用于Web应用系统的开发。
因为其本身就用于身份验证和权限控制,出现身份验证绕过的问题是比较严重的,CVSS给的评分就比较高
CVE-2020-1957 CVSS 3.x 评分 9.8,CVSS 2.0评分 7.5
CVE-2020-11989 CVSS 3.x 评分 9.8,CVSS 2.0评分 7.5
CVE-2020-13933 CVSS 目前还未评分,后续补上
Shiro是基于URI的权限认证,配置的url模式使用Ant风格模式。Ant路径通过通配符支持“?”、“*”、“**”。
对于“?”,其匹配一个字符串。如“/admin?”将匹配“admin1”,但不匹配“/admin”或“/admin/”。
对于“*”,其匹配零个或多个字符串。如“/admin/*”将匹配“/admin/”、“/admin/abc”,但不匹配“/admin/a/b”。
对于“**”,其匹配路径中的零个或多个路径。如“/admin/**”将匹配“/admin/a”或“/admin/a/b”。
常用的拦截器配置:
anon(anonymous)拦截器表示匿名访问(既不需要登录即可访问)。
authc(authentication)拦截器表示需要身份认证过后才能访问。
拦截器的匹配顺序采取第一次匹配优先的方式,即从头开始使用第一个匹配的url模式对应的拦截器链。
例如在配置中存在如下配置:
1 | Map<String, String> map = new LinkedHashMap<>(); |
/doLogin
可以直接被访问,而/admin/*
和/manage/*
需要进行身份认证。
SHIRO搭建
测试Demo:https://github.com/s31k31/shiro-simple-example
导入IDE,等待maven配置加载完成,点击运行/调试即可启动。
需测试不同版本的shiro时,在pom.xml文件中修改shiro版本即可。
src/main/resources/application.properties
中配置的端口为8011,context path
为/shiro
,有需要可以自行更改。
SHIRO-682
https://issues.apache.org/jira/browse/SHIRO-682
在2019年3月,中国开发者tomsun08在shiro项目中提出PR
在Spring web中
/resource/menus
和resource/menus/
都能访问到同一资源。而shiro中的
pathPattern
只能匹配/resource/menus
,而不能匹配/resource/menus/
。用户使用
requestURI + "/"
就能绕过权限控制。
但直到2019年11月,在shiro 1.5.0中修复这一问题。修改的代码在commit
tomsum08不是专门的安全研究人员,所以当时仅对URI最后的/
做了处理。若URI最后为/
,则去掉该/
。
这处改动的并非真正漏洞核心,漏洞本质是Spring处理URI和Shiro处理URI不一致导致的。
Spring处理URI和Shiro处理URI不一致性才是导致后续多个漏洞曝出的真正原因。
CVE-2020-1957
漏洞存在于1.5.2版本之前,复现时将pom.xml
中的shiro版本设置为1.5.2版本之前,如下设置为1.5.1
1 | <dependency> |
漏洞复现
访问:http://localhost:8011/shiro/admin/page ,此时跳转登录页面
访问:http://localhost:8011/shiro/xxxx/..;/admin/page ,成功绕过身份校验
漏洞分析
shiro配置规则
1 | Map<String, String> map = new LinkedHashMap<>(); |
org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain
是shiro判断输入的URI是否匹配拦截器的函数。匹配成功将返回相应的拦截器,进行对应的权限操作。
当我们传入/shiro/admin/page
时(/shiro
在此属于context path,不会被当作URI),会匹配到/admin/*
这条规则,从而进行authc,也就是到org/apache/shiro/web/filter/authc/AuthenticatingFilter.java
中去判断权限。
但当我们传入/shiro/xxxx/..;/admin/page
时,shiro获取到的URI是/xxxx/..
,注意这里shiro移除了;
后面的内容。
我们跟进getPathWithinApplication()
中进一步查看,最终调用的是org.apache.shiro.web.util.WebUtils#getPathWithinApplication
,可以看到requestUri
是通过getRequestUri()
方法获取得到的
getRequestUri()
代码如下,根据request.getRequestURI()
获取的uri,传入decodeAndCleanUriString()
方法。关于request.getRequestURI
带来的安全问题可查看这篇文章。
decodeAndCleanUriString()
方法对;
后面的内容进行删除,只获取;
前的内容。所以我们传入的/xxxx/..;/admin/page
,在shiro中得到的只是/xxxx/..
1 | public static String getRequestUri(HttpServletRequest request) { |
/xxxx/..
没有匹配到shiro配置中的规则,默认放行。
但在spring web处理/shiro/xxxx/..;/admin/page
时,Spring对..;/
是包容的,会被当成../
处理,所以最后访问的是/shiro/admin/page
。
当然/shiro/xxxx;/../admin/page
同样能绕过身份验证
这就是由于shiro和spring对URL处理的不一致导致的第一个漏洞,后续的CVE-2020-11989和CVE-2020-13933属于同一类问题。
漏洞修复
漏洞修复代码在此commit,将request.getRequestURI()
改成了request.getContextPath()+"/"+request.getServletPath()+request.getPathInfo()
getServletPath
得到的是实际Servlet路径,无法利用路径回溯../
和分号;
绕过。
在shiro1.5.2版本中,传入/shiro/xxxx/..;/admin/page
,得到的结果是/shiro//admin/page
CVE-2020-11989
该漏洞由淚笑向 Apache Shiro 官方报告的漏洞。
漏洞存在于1.5.3版本之前,复现时将pom.xml
中的shiro版本设置为1.5.2版本
漏洞复现
在CVE-2020-1957中的漏洞修复中提到request.getServletPath
只返回Servlet实际的路径,但是request.getContextPath
是能获取到分号;
的。
于是访问:http://localhost:8011/shiro;/admin/page 或 http://localhost:8011/;/shiro/admin/page ,实现绕过。
漏洞分析
当传入/shiro;/admin/page
时,request.getContextPath
得到的是/shiro;/
,request.getServletPath
得到的是/admin/page
decodeAndCleanUriString
方法中的代码没有变动,还是删除;
后面的内容进行,只获取;
前的内容。
最后shiro得到的URI是/shiro;
,不会匹配到身份认证校验规则,默认放行。
⚠️:该绕过方式一定需要有context path的配置,如果没有系统没有设置context path,request.getContextPath
默认为空。
除此之外,腾讯玄武实验室的Ruilin发现另一个绕过方法。
shiro配置的规则
1 | map.put("/manage/*", "authc"); |
对应的Controller
1 | "/manage/{name}") ( |
http://localhost:8011/shiro/manage/test
http://localhost:8011/shiro/manage/test%25%32%46test
原因是在decodeAndCleanUriString()
方法中存在decodeRequestString
用于URL解码。
1 | private static String decodeAndCleanUriString(HttpServletRequest request, String uri) { |
shiro二次解码得到的是/shiro/manage/test/test
,因为鉴权规则设置的是/manage/*
一个星号,只匹配一层目录,/shiro/manage/test/test
,算是两层目录,也就不属于/manage/*
。而Spring解析时只会将URI解码一次,得到的是/shiro/manage/test%2ftest
,从而绕过访问。
该绕过的场景更严苛一些,可利用场景稍少。
漏洞修复
官方更改了URI的获取逻辑,使用移除分号后的request.getRequestURI
和request.getPathInfo()
进行URI拼接。并且没有用decodeAndCleanUriString()
方法处理URI,避免了二次URL解码。
CVE-2020-13933
该漏洞由蚂蚁非攻实验室codeplutos提交,具体可看这里。其中说到新增了Global Filter
来缓解该漏洞。
漏洞存在于1.6.0版本之前,复现时将pom.xml
中的shiro版本设置为1.5.3版本,该漏洞触发条件稍微苛刻一些,也是需要将请求映射成路径变量的形式。
漏洞复现
Controller对应代码:
1 | "/manage/{name}") ( |
访问:http://localhost:8011/shiro/manage/index ,302跳转到登录页面
访问:http://localhost:8011/shiro/manage/%3Bindex ,绕过身份验证
漏洞分析
查看漏洞修复的commit,其中新增了一个全局过滤器,路径为web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java
,该过滤器对分号;
、反斜杠\
、和ascii不可打印字符的处理。
1 | private static final List<String> SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B")); |
当WebUtils.toHttp(request).getRequestURI()
获取到的URI存在分号;
、反斜杠\
、ascii不可打印字符时,抛出400错误。
当我们传入/manage/%3Bindex
时,shiro得到的是/manage/
而spring得到的是/manage/;index
,;index
作为{name}
参数
所以能绕过shiro的身份认证
漏洞修复
把shiro版本设置成1.6.0,再次访问,此时URI中存在分号;
,返回400错误。
参考
https://shiro.apache.org/security-reports.html
https://l3yx.github.io/2020/06/30/Shiro-权限绕过漏洞-CVE-2020-11989/