今年以来,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/