目录导航
前言
本文继续漏洞原理系列文章,这次简单的学习和分析了权限校验框架 Shiro 在历史上爆出的共 11 个带有 CVE 编号的漏洞,根据每个 CVE 漏洞的原理,版本更新的代码来分析安全漏洞产生的原理、利用方式、特性、坑。
主要还是对基础洞的学习,用来积累思路和姿势,目前对于 Java 安全来讲,单一的基础洞只能打打垃圾站,能够普遍适用的漏洞还需要组合拳来搞定,因此对思路、姿势的积累愈发重要,任何一个小 tricks 都能成为日后组链的关键点。
个人能力有限,如文章中有描述不清、有偏差甚至是错误的情况,希望师傅们不吝赐教。
目录
点击左边连接可以直接跳到对应漏洞的调试记录。
链接 | 描述 |
---|---|
简介 | Apache Shiro 简介 |
初识 | 几个关键类的介绍 |
使用 | 在 Servlet 项目和 Spring 项目中的简单使用 |
CVE-2010-3863 | 由于未标准化路径导致的绕过 |
CVE-2014-0074 | 使用 ldap 服务器认证时两个场景的绕过 |
CVE-2016-4437 | RememberMe 反序列化漏洞 |
CVE-2016-6802 | Context Path 路径标准化导致绕过 |
CVE-2019-12422 | Padding Oracle Attack & CBC Byte-Flipping Attack |
CVE-2020-1957 | Spring 与 Shiro 对于 “/” 和 “;” 处理差异导致绕过 |
CVE-2020-11989 | Shiro 二次解码导致的绕过以及 ContextPath 使用 “;” 的绕过 |
CVE-2020-13933 | 由于 Shiro 与 Spring 处理路径时 URL 解码和路径标准化顺序不一致 导致的使用 “%3b” 的绕过 |
CVE-2020-17510 | 由于 Shiro 与 Spring 处理路径时 URL 解码和路径标准化顺序不一致 导致的使用 “%2e” 的绕过 |
CVE-2020-17523 | Shiro 匹配鉴权路径时会对分隔的 token 进行 trim 操作 导致的使用 “%20” 的绕过 |
CVE-2021-41303 | 由于 Shiro 的 BUG 导致特定场景的绕过(不确定) |
总结
通过对 Shiro 漏洞的学习和调试,我们对 Shiro 的一些技术的实现和安全部署有了一定的了解。除了 SHIRO-550 和 SHIRO-721 的反序列化以及 CVE-2014-0074 的 ldap 绕过之外,其他的绕过都是在路径处理过程中产生问题导致的绕过。
这些绕过多数是由于 shiro 的处理逻辑有误,或和中间件、其他框架的处理逻辑不一致导致的安全问题,通常会依赖场景。
更多时候,在真实的环境中,开发人员自己的配置也会导致鉴权的绕过,例如配置顺序、配置中是否有空格、配置中一些特殊符号的使用、Ant 表达式使用差异、开发人员鉴权代码逻辑有误等等,这部分目前在文章中没有涉及,后续会考虑补上。
另外,随着一些转发中间件、API 中间件等等中间层的介入,会扰乱 shiro 的鉴权配置,也会导致很多的安全问题。
可以预见的是,在特定场景下的绕过还是会出现的。
在 Shiro 的修复过程中还能看到的是,shiro 会提供解决方案,但有时不是升级版本就可以的,也有时修复的也并不全面,所以实现 shiro 安全还是需要安全知识和经验的加持。
杂记汇总
在整个的学习和调试过程中,有一些觉得有趣的杂记,放在这里供大家查看。
Spring 版本
Spring 的版本会完全的影响漏洞的触发,在某些 CVE 中,低版本生效,高版本不生效,在某些 CVE 中则相反,还有时由于 Spring 版本高可以绕过 Shiro 的更新补丁。
这些在具体的漏洞分析都提到了,实际上是因为 alwaysUseFullPath
导致的,详细的内容可以自行了解。
Ant 表达式中的 “*”
在 Ant 表达式中的 “*” 撑起了 shiro bypass 的大旗。其中关键点用一句话解释就是:
/audit/*
不能匹配/audit/
也不能匹配/audit/a/
彩蛋
在翻 Shiro 的 ISSUES 时,发现李三师傅提交的 SHIRO-760 提到了在使用 Tomcat AJP 时会导致的绕过问题。
但是官方认为其不算漏洞,就没下文了。可以预想到,关于不同协议下对请求路径的处理差异、或能够控制 Attribute 的场景下也可能导致绕过的产生。
Tips
这里分享几个分析时的小技巧。
第一,通过如下链接就可以查看这个版本修复了哪些 ISSUES ,方便定位。
https://issues.apache.org/jira/issues/?jql=project%20%3D%20SHIRO%20AND%20fixVersion%20%3D%201.5.2
第二,通过 diff 版本来查看差异代码分析漏洞,语法如下。
例如:https://github.com/apache/shiro/compare/shiro-root-1.7.0…shiro-root-1.7.1
反序列化
目前针对 Shiro 讨论最多的,还是 RememberMe 反序列化漏洞的延伸和姿势,我大概过了一下全网的文章,主要包括以下几个点:
- Shiro 组件的检测:检测站点是否包含 shiro 组件,cookie 关键字不是 rememberMe 等情况;
- Shiro AES 弱密钥的检测:检测 Shiro 是否内置或配置了常见的弱密钥;
- Shiro 内置链的利用:无 CC 依赖的 CB 链反序列化利用;
- 配合 RMI 利用:处理由于插入反序列化链导致的 Header 长度的问题,以及 Transformer 数组加载不到报错问题;
- 其他绕过 Tomcat Header 长度的姿势:反射修改 AbstractHttp11Protocol 的 maxHeaderSize、gzip + base64压缩编码、从外部或从 HTTP 请求 body 中加载类字节码;
- Ysoserial 改造:由于 shiro RememberMe 反序列化流程中加载类方式不同导致需要对 ysoserial 中 CC 等链的改造;
- 组合攻击:在 weblogic/Tomcat 等中间件上完成 shiro 的攻击、gadget 的利用、内存马的写入等组合操作;
- 改 Key:对于弱加密密钥,在攻击后将其修改,让目标仅为自己所用。
这部分内容算是进阶内容,没有放在本篇用于入门学习的笔记类文章中。感兴趣的同学可以在先知和 Seebug Paper 中搜索关键字 Shiro 来了解。
前言
Shiro 是这一阶段比较火的攻击点,由于其适用范围广泛,每次爆发漏洞危害通常较大,本文将会梳理、总结和学习其相关漏洞。
简介
Apache Shiro 是一个 Java 安全框架,包括如下功能和特性:
- Authentication:身份认证/登陆,验证用户是不是拥有相应的身份。在 Shiro 中,所有的操作都是基于当前正在执行的用户,这里称之为一个
Subject
,在用户任意代码位置都可以轻易取到这个Subject
。Shiro 支持数据源,称之为Realms
,可以利用其连接 LDAP\AD\JDBC 等安全数据源,并支持使用自定义的Realms
,并可以同时使用一个或多个Realms
对一个用户进行认证,认证过程可以使用配置文件配置,无需修改代码。同时,Shiro 还支持 RememberMe,记住后下次访问就无需登录。 - Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限。同样基于
Subject
、支持多种Realms
。Shiro 支持Wildcard Permissions
,也就是使用通配符来对权限验证进行建模,使权限配置简单易读。Shiro 支持基于Roles
和基于Permissions
两种方式的验证,可以根据需要进行使用。并且支持缓存产品的使用。 - Session Manager:会话管理,用户登陆后就是一次会话,在没有退出之前,它的所有信息都在会话中。Shiro 中的一切(包括会话和会话管理的所有方面)都是基于接口的,并使用 POJO 实现,因此可以使用任何与 JavaBeans 兼容的配置格式(如 JSON、YAML、Spring XML 或类似机制)轻松配置所有会话组件。Session 支持缓存集群的方式;还支持事件侦听器,允许在会话的生命周期内侦听生命周期事件,以执行相关逻辑。Shiro Sessions 保留发起会话的主机的 IP 地址,因此可以根据用户位置来执行不同逻辑。Shiro 对 Web 支持实现了
HttpSession
类及相关全部 API。也可以在 SSO 中使用。 - Cryptography:加密,保护数据的安全性;Shiro 专注于使用公私钥对数据进行加密,以及对密码等数据进行不可逆的哈希。
- Permissions:用户权限;Shiro 将所有的操作都抽象为 Permission,并默认使用
Wildcard Permissions
来进行匹配。Shiro 支持实例级别的权限控制校验,例如domain:action:instance
。 - Caching:缓存,为了提高 Shiro 在业务中的性能表现。Shiro 的缓存支持基本上是一个封装的 API,由用户自行选择底层的缓存方式。缓存中有三个重要的接口
CacheManager
/Cache
/CacheManagerAware
,Shiro 提供了默认的MemoryConstrainedCacheManager
等实现。
初识
在使用 Shiro 前,先来看一下其中几个关键组件,有助于后面更好的分析相关漏洞。
SecurityManager
org.apache.shiro.mgt.SecurityManager
是 shiro 的一个核心接口,接口负责了一个 Subject 也就是“用户”的全部安全操作:
- 接口本身定义了
createSubject
、login
、logout
三个方法用来创建 Subject、登陆和退出。 - 扩展了
org.apache.shiro.authc.Authenticator
接口,提供了authenticate
方法用来进行认证。 - 扩展了
org.apache.shiro.authz.Authorizer
接口,提供了对 Permission 和 Role 的校验方法。包括has/is/check
相关命名的方法。 - 扩展了
org.apache.shiro.session.mgt.SessionManager
接口,提供了start
、getSession
方法用来创建可获取会话。
Shiro 为 SecurityManager 提供了一个包含了上述所有功能的默认实现类 org.apache.shiro.mgt.DefaultSecurityManager
,中间继承了很多中间类,并逐层实现了相关的方法,继承关系如下图。

DefaultSecurityManager 中包含以下属性:
subjectFactory
:默认使用 DefaultSubjectFactory,用来创建具体 Subject 实现类。subjectDAO
:默认使用 DefaultSubjectDAO,用于将 Subject 中最近信息保存到 Session 里面。rememberMeManager
:用于提供 RememberMe 相关功能。sessionManager
:默认使用 DefaultSessionManager,Session 相关操作会委托给这个类。authorizer
:默认使用 ModularRealmAuthorizer,用来配置授权策略。authenticator
:默认使用 ModularRealmAuthenticator,用来配置认证策略。realm
:对认证和授权的配置,由用户自行配置,包括 CasRealm、JdbcRealm 等。cacheManager
:缓存管理,由用户自行配置,在认证和授权时先经过,用来提升认证授权速度。
DefaultSecurityManager 还有一个子类,就是 org.apache.shiro.web.mgt.DefaultWebSecurityManager
,这个类在 shiro-web 包中,是 Shiro 为 HTTP/SOAP 等 http 协议连接提供的实现类,这个类默认创建配置了 org.apache.shiro.web.mgt.CookieRememberMeManager
用来提供 RememberMe 相关功能。
Subject
org.apache.shiro.subject.Subject
是一个接口,用来表示在 Shiro 中的一个用户。因为在太多组件中都使用了 User
的概念,所以 Shiro 故意避开了这个关键字,使用了 Subject
。
Subject 接口同样提供了认证(login/logout)、授权(访问控制 has/is/check 方法)以及获取会话的能力。在应用程序中如果想要获取一个当前的 Subject,通常使用 SecurityUtils.getSubject()
方法即可。
单从方法的命名和覆盖的功能来看,Subject 提供了与 SecurityManager 非常近似的方法,用来执行相关权限校验操作。而实际上,Subject 接口在 core 包中的实现类 org.apache.shiro.subject.support.DelegatingSubject
本质上也就是一个 SecurityManager 的代理类。
DelegatingSubject 中保存了一个 transient 修饰的 SecurityManager 成员变量,在使用具体的校验方法时,实际上委托 SecurityManager 进行处理,如下图:

DelegatingSubject 中不会保存和维持一个用户的“状态(角色/权限)”,恰恰相反,每次它都依赖于底层的实现组件 SecurityManager 进行检查和校验,因此通常会要求 SecurityManager 的实现类来提供一些缓存机制。所以本质上,Subject 也是一种“无状态”的实现。
Realm
Realm 翻译过来是“领域、王国”,这里可以将其理解以为一种“有界的范围”,实际上就是权限和角色的认定。
org.apache.shiro.realm.Realm
是 Shiro 中的一个接口,Shiro 通过 Realm 来访问指定应用的安全实体——用户、角色、权限等。一个 Realm 通常与一个数据源有 1 对 1 的对应关系,如关系型数据库、文件系统或者其他类似的资源。
因此,此接口的实现类,将使用特定于数据源的 API 来进行认证或授权,如 JDBC、文件IO、Hibernate/JPA 等等,官方将其解释为:特定于安全的 DAO 层。
在使用中,开发人员通常不会直接实现 Realm 接口,而是实现 Shiro 提供了一些相关功能的抽象类 AuthenticatingRealm/AuthorizingRealm,或者使用针对特定数据源提供的实现类如 JndiLdapRealm/JdbcRealm/PropertiesRealm/TextConfigurationRealm/IniRealm 等等。继承关系大概如下:

较多情况下,开发人员会自行实现 AuthorizingRealm
类,并重写 doGetAuthorizationInfo
/doGetAuthenticationInfo
方法来自行实现自身的认证和授权逻辑。
小结
通过对以上三个组件的了解,一次认证及授权的校验流程就形成了:
- 应用程序通过获取当前访问的 Subject(也就是用户),并调用其相应校验方法;
- Subject 将校验委托给 SecurityManager 进行判断;
- SecurityManager 会调用 Realm 来获取信息来判断用户对应的角色能否进行操作。
使用
本章来看一下该如何将 Shiro 安全框架集合在 web 应用中,官方文档 Web Support 一章给出了一些使用方法,这里进行学习和测试。
web.xml
在普通 web 项目中, Shiro 框架的注入是通过在 web.xml
中配置 Filter 的方式完成的。
在 Shiro 1.1 及之前的版本,通过配置 IniShiroFilter
,并在 /WEB-INF/shiro.ini
或 classpath:shiro.ini
中进行相应的权限配置。也可以指定配置文件路径,示例如下:
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
<init-param>
<param-name>configPath</param-name>
<param-value>/WEB-INF/anotherFile.ini</param-value>
</init-param>
</filter>
在 Shiro 1.2 及之后的版本,可以进行如下配置:
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
官方更推荐直接使用 ShiroFilter
类进行处理,并为 Web 应用程序配置了一个 Listener: EnvironmentLoaderListener
。这是一个 ServletContextListener
的子类,会在初始化时将 WebEnvironment 的实现类注入到 ServletContext 中。

ShiroFilter 则使用 WebEnvironment 中的 WebSecurityManager 来作为当前 Shiro 上下文中的 SecurityManager。

在 Filter 处理流程中,ShiroFilter 继承的 doFilter
调用 AbstractShiroFilter#doFilterInternal
方法,会使用保存的 SecurityManager 创建 Subject 对象。

并调用其 execute 方法执行后续的校验逻辑。

默认情况下,EnvironmentLoaderListener
创建的 WebEnvironment 的实例是 IniWebEnvironment,是基于 INI 格式的配置文件,如果不想使用这个格式,可以通过自实现一个 IniWebEnvironment 的子类,用来处理自己定义的配置文件格式,并在 web.xml
中进行如下配置:
<context-param>
<param-name>shiroEnvironmentClass</param-name>
<param-value>org.su18.shiro.web.config.WebEnvironment</param-value>
</context-param>
关于 INI 配置文件的配置,在官方文档配置一章有详细描述,主要包括 [main]
、[users]
、[roles]
、[urls]
四项配置。如果配置了 [users]
或 [roles]
,则会自动创建 org.apache.shiro.realm.text.IniRealm
实例,并可以在 [main]
配置中进行调用及配置。
这里重点的配置,就在于 [urls]
这个配置项,详情参考相关官方配置文档。大概可以配置成如下形式:
[urls]
/index = anon
/user/** = authc
/admin/** = authc, roles[administrator]
/audit/** = authc, perms["remote:invoke"]
简单来说,就是一个 Ant 风格的路径表达式与需要处理他的 Filter 之间的映射。Shiro 使用 org.apache.shiro.web.filter.mgt.FilterChainManager
自己维护一套 FilterChain 的机制,用来依次对多个 Filter 进行校验。
Shiro 默认提供了一些 Filter,名称及对应处理类如下表格,如果想深入理解某个 Filter 功能的具体实现,可以具体查看。
在请求访问到达 ShiroFilter 后,会根据 request 的信息,调用 org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain
方法匹配配置的 pathPattern 以及 requestURI,如果有匹配,则会添加一层 ProxiedFilterChain 代理。这里看到,如果 pathMatches
方法匹配,将会进行 return,因此配置的顺序也很重要。

也就是说,Shiro 不会向 Servlet Context 中添加其他的 Filter,而是使用嵌套 ProxiedFilterChain 代理的方式扩展 FilterChain,并在自身 Filter 都处理结束之后继续执行原 FilterChain。

这里对于 Servlet Filter/FilterChain 以及 Shiro Filter/FilterChain 的区分描述可能不清晰,其实只需要自己下个断点跟一下流程就能明白了。
Spring
在目前的环境下,越来越多的 Web 环境使用了 SpringBoot/SpringMVC 及相关生态,因此更多的时候会将 Shiro 集成配置在其中。为了应对此环境,Shiro 提供了 shiro-spring
包来进行配置。
在 Servlet 项目中,是通过在 web.xml
中配置了能匹配所有 URL 路径 /*
的 ShiroFilter,并由其执行后续逻辑。而在 Spring 生态下,由于 IoC 与 DI 的思想,通常把所有的 Filter 注册成为 Bean 交给 Spring 来管理。
此时如果想要将 Shiro 逻辑注入其中,就用到了关键类:ShiroFilterFactoryBean
。这是 Shiro 为 Spring 生态提供的工厂类,由它在 spring 中承担了之前 ShiroFilter 的角色。内部类 SpringShiroFilter 继承了 AbstractShiroFilter,实现了类似的逻辑。

可以结合 spring-web
包中的 DelegatingFilterProxy 配置使用,其作用就是一个 filter 的代理,被它代理的 filter 将由 spring 来管理其生命周期。

ShiroFilterFactoryBean 还是 BeanPostProcessor 的子类,实现了对于 Filter 子类自动发现和处理的技术,所以我们可以通过配置 ShiroFilterFactoryBean 的方式来注册 SpringShiroFilter。

其他的配置也可以全部交由 Spring 管理,我们只需要对 ShiroFilterFactoryBean 进行配置即可,简单的示例代码如下:
/**
* @author su18
*/
@Configuration
public class ShiroConfig {
@Bean
MyRealm myRealm() {
return new MyRealm();
}
@Bean
RememberMeManager cookieRememberMeManager() {
return new CookieRememberMeManager();
}
@Bean
SecurityManager securityManager(MyRealm myRealm, RememberMeManager cookieRememberMeManager) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm((Realm) myRealm);
manager.setRememberMeManager(cookieRememberMeManager);
return manager;
}
@Bean(name = {"shiroFilter"})
ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/index/login");
bean.setUnauthorizedUrl("/index/unauth");
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
map.put("/index/user", "authc");
map.put("/index/**", "anon");
map.put("/audit/**", "authc, perms[\"audit:list\"]");
map.put("/admin/**", "authc, roles[admin]");
map.put("/logout", "logout");
bean.setFilterChainDefinitionMap(map);
return bean;
}
}
安全漏洞
由于 Shiro 本身作为一个安全校验框架,所以其安全漏洞包含自身存在的安全问题,也包含能导致其安全校验失效的相关漏洞。
根据官方网站上的漏洞通报,shiro 在历史上共通报了 11 个 CVE,其中包含认证绕过、反序列化等漏洞类型,接下来我们来依次学习。
CVE-2010-3863
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2010-3863 / CNVD-2010-2715 |
影响版本 | shiro < 1.1.0 & JSecurity 0.9.x |
漏洞描述 | Shiro 在对请求路径与 shiro.ini 配置文件配置的 AntPath 进行对比前 未进行路径标准化,导致使用时可能绕过权限校验 |
漏洞关键字 | /./ & 路径标准化 |
漏洞补丁 | Commit-ab82949 |
相关链接 | https://vulners.com/nessus/SHIRO_SLASHDOT_BYPASS.NASL https://marc.info/?l=bugtraq&m=128880520013694&w=2 |
漏洞详解
之前提到过,Shiro 使用 PathMatchingFilterChainResolver#getChain
方法获取和调用要执行的 Filter,逻辑如下:

getPathWithinApplication()
方法调用 WebUtils.getPathWithinApplication()
方法,用来获取请求路径。通过如下逻辑可看到,方法获取 Context 路径以及 URI 路径,然后使用字符串截取的方式去掉 Context 路径。

获取 URI 路径的方法 getRequestUri()
获取 javax.servlet.include.request_uri
的值,并调用 decodeAndCleanUriString()
处理。

decodeAndCleanUriString()
是 URL Decode 及针对 JBoss/Jetty 等中间件在 url 处添加 ;jsessionid
之类的字符串的适配,对 ;
进行了截取。

处理之后的请求 URL 将会使用 AntPathMatcher#doMatch
进行匹配尝试。
流程梳理到这里就出现了一个重大的问题:在匹配之前,没有进行标准化路径处理,导致 URI 中如果出现一些特殊的字符,就可能绕过安全校验。比如如下配置:
[urls]
/user/** = authc
/admin/list = authc, roles[admin]
/admin/** = authc
/audit/** = authc, perms["audit:list"]
/** = anon
在上面的配置中,为了一些有指定权限的需求的接口进行了配置,并为其他全部的 URL /**
设置了 anno
的权限。在这种配置下就会产生校验绕过的风险。
正常访问:/audit
,会由于需要认证和权限被 Shiro 的 Filter 拦截并跳转至登录 URL。

访问 /./audit
,由于其不能与配置文件匹配,导致进入了 /**
的匹配范围,导致可以越权访问。

漏洞修复
Shiro 在 ab82949 更新中添加了标准化路径函数。

对 /
、//
、/./
、/../
等进行了处理。

CVE-2014-0074
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2014-0074 / CNVD-2014-03861 / SHIRO-460 |
影响版本 | shiro 1.x < 1.2.3 |
漏洞描述 | 当程序使用LDAP服务器并启用非身份验证绑定时,远程攻击者可借助 空的用户名或密码利用该漏洞绕过身份验证。 |
漏洞关键字 | ldap & 绕过 & 空密码 & 空用户名 & 匿名 |
漏洞补丁 | Commit-f988846 |
相关链接 | https://stackoverflow.com/questions/21391572/shiro-authenticates…in-ldap https://www.openldap.org/doc/admin24/security.html |
漏洞详解
首先来复现一下这个漏洞,搭建一个 ldap 服务器用于认证,这里作者在测试时尝试使用了 openldap 的 docker 环境:
docker pull osixia/openldap
docker run -p 389:389 -p 636:636 --name openldap --network bridge \
--hostname openldap-host --env LDAP_ORGANISATION="su18" \
--env LDAP_DOMAIN="su18.org" --env LDAP_ADMIN_PASSWORD="123456" \
--detach osixia/openldap
以及 mac 自带的 openldap 环境,
sudo /usr/libexec/slapd -f /etc/openldap/slapd.conf -d 255
启动后随意向其中添加一个用户。shiro.ini
采用如下配置:
[main]
# 登陆地址
authc.loginUrl = /login
# ldap
ldapContextFactory = org.apache.shiro.realm.ldap.JndiLdapContextFactory
ldapContextFactory.url = ldap://127.0.0.1:389/
# realm
adRealm = org.apache.shiro.realm.activedirectory.ActiveDirectoryRealm
adRealm.ldapContextFactory = $ldapContextFactory
adRealm.searchBase = "cn=config,dc=su18,dc=org"
[urls]
/index = anon /login = anon /logout = logout /** = authc
按照 BUG 提交者的配置,设置 Realm 为 ActiveDirectoryRealm,并指定其 ldapContextFactory 为 JndiLdapContextFactory。BUG 提交者一共提出了两个场景,一个是空账户加空密码绕过,一个是空账户加任意密码绕过。
根据官方通告是 ldap 服务器在 enabled 了 unauthenticated bind 之后会受到影响,这里来复现一下。
场景 1
场景 1 是当 ldap 服务器允许匿名访问(Anonymous)时,可以使用空用户和空密码登录,复现如下。
首先访问 /login
接口登陆,在我搭建的测试环境中,访问链接:http://localhost:8080/login?username=cn=test,dc=su18,dc=org&password=test,成功登陆后,页面跳转至 /user
,显示认证后才会看到的页面,并打印出了当前用户的 principal。

此时一切认证状态正常。随后访问 /logout
接口登出,页面跳转回 /login
登陆页面。

接下来就是见证奇迹的时刻,再次尝试登陆,使用空用户名及空密码,访问链接:http://localhost:8080/login?username=&password=,发现成功认证,页面跳转至 /user
,可以访问到需要认证才展示的页面,而 SecurityUtils.getSubject().getPrincipal()
的结果为 ""
。

其他需要认证的页面也可以直接访问,如 /admin
。

场景 2
首先修改 openldap 的配置文件开启未授权 bind,如下图配置:

接下来使用空用户名+任意密码的组合尝试登陆,访问链接:http://localhost:8080/login?username=&password=123,
发现同样会成功登陆,页面跳转至 /user
,同样 principal 为空字符串。

这个漏洞的调用我从头到尾跟了好几次,但这里并不打算列举出来调用链,或分析判断逻辑,因为从两个场景来说,漏洞本质上应该是 ldap 的配置问题,并不应作为 Shiro 的安全漏洞被列举出来,因为不同机制的实现肯定有差异。但官方还是出了更新补丁,甚至给了 CVE,很让人费解。
但这还不是最让人费解的,最让人费解的是这个 CVE 的修复补丁逻辑。
漏洞修复
Shiro 在 f988846 中针对此漏洞进行了修复,实际上,整个 1.2.3 版本的更新就是针对这个漏洞。
官方在 DefaultLdapContextFactory
和 JndiLdapContextFactory
中均加入了 validateAuthenticationInfo
方法用来校验 principal 和 credential 为空的情况。可以看到这里的逻辑是只有 principal 不为空的情况下,才会对 credential 进行校验。

并在 getLdapContext
方法创建 InitialLdapContext 前执行了校验,如果为空,将会抛出异常。

修复看到这里就让人有些摸不到头脑,正常来讲,本次漏洞的修复应该针对 BUG 提交者提到的空用户名绕过的安全问题,也就是如下两种场景:
- ldap unauthenticated bind enabled 的情况下,可以使用空用户名+任意密码进行认证。
- ldap allow anonymous 的情况下,可以空用户名+空密码的匿名访问进行认证。
这两种均是 Shiro 判断机制和 ldap 配置之间冲突导致的问题,但是 shiro 并未修复这两种情况,而修复的是有用户名但是密码是空的情况,这种机制在 ldap 中不叫 unauthenticated,实际叫做 Pass-Through Authentication。LDAP 服务器在开启了相关配置后,允许通过用户名+空密码/错误密码的方式来经过认证。
很显然,Shiro 是针对这种情况进行了修复,可能是对提交的 BUG 理解有误,但它确实修复了一项漏洞,只不过这修复的和提交的 BUG 关系并不大。所以…你懂得。
CVE-2016-4437
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2016-4437 / CNVD-2016-03869 / SHIRO-550 |
影响版本 | shiro 1.x < 1.2.5 |
漏洞描述 | 如果程序未能正确配置 “remember me” 功能使用的密钥。 攻击者可通过发送带有特制参数的请求利用该漏洞执行任意代码或访问受限制内容。 |
漏洞关键字 | cookie & RememberMe & 反序列化 & 硬编码 & AES |
漏洞补丁 | Commit-4d5bb00 |
相关链接 | SHIRO-441 https://www.anquanke.com/post/id/192619 |
漏洞详解
Shiro 从 0.9 版本开始设计了 RememberMe 的功能,用来提供在应用中记住用户登陆状态的功能。
RememberMeManager
首先是接口 org.apache.shiro.mgt.RememberMeManager
,这个接口提供了 5 个方法:
getRememberedPrincipals
:在指定上下文中找到记住的 principals,也就是 RememberMe 的功能。forgetIdentity
:忘记身份标识。onSuccessfulLogin
:在登陆校验成功后调用,登陆成功时,保存对应的 principals 供程序未来进行访问。onFailedLogin
:在登陆失败后调用,登陆失败时,在程序中“忘记”该 Subject 对应的 principals。onLogout
: 在用户退出时调用,当一个 Subject 注销时,在程序中“忘记”该 Subject 对应的 principals。
之前曾在 DefaultSecurityManager 的成员变量中见到了 RememberMeManager 成员变量,会在登陆、认证等逻辑中调用其中的相关方法。
AbstractRememberMeManager
同时,Shiro 还提供了一个实现了 RememberMeManager
接口的抽象类 AbstractRememberMeManager
,提供了一些实现技术细节。先介绍其中重要的几个成员变量:
DEFAULT_CIPHER_KEY_BYTES
:一个 Base64 的硬编码的 AES Key,也是本次漏洞的关键点,这个 key 会被同时设置为加解密 key 成员变量:encryptionCipherKey/decryptionCipherKey 。serializer
:Shiro 提供的序列化器,用来对序列化和反序列化标识用户身份的 PrincipalCollection 对象。cipherService
:用来对数据加解密的类,实际上是org.apache.shiro.crypto.AesCipherService
类,这是一个对称加密的实现,所以加解密的 key 是使用了同一个。
在其初始化时,会创建 DefaultSerializer
作为序列化器,AesCipherService
作为加解密实现类,DEFAULT_CIPHER_KEY_BYTES
作为加解密的 key。

CookieRememberMeManager
在 shiro-web 包中提供了具体的实现类 CookieRememberMeManager,实现了在 HTTP 无状态协议中使用 cookie 记录用户信息的相关能力。其中一个比较重要的方法是 getRememberedSerializedIdentity
,具体逻辑如下图:

简单来说就是获取 Cookie 中的内容并 Base64 解码返回 byte 数组。
漏洞点
大概了解上面几个类的关键点后,就可以来关注这个漏洞的利用了。之前提到过,在 Filter 处理流程中,无论是 ShiroFilter 还是 IniShiroFilter, doFilter
方法都是继承至 AbstractShiroFilter,会调用 AbstractShiroFilter#doFilterInternal
方法,使用保存的 SecurityManager 创建 Subject 对象。具体调用流程大概如下:
AbstractShiroFilter.doFilterInternal()
AbstractShiroFilter.createSubject()
WebSubject.Builder.buildWebSubject()
Subject.Builder.buildSubject()
DefaultSecurityManager.createSubject()
DefaultSecurityManager.resolvePrincipals()
DefaultSecurityManager.getRememberedIdentity()
AbstractRememberMeManager.getRememberedPrincipals()
CookieRememberMeManager.getRememberedSerializedIdentity()
创建 Subject 对象后,会试图从利用当前的上下文中的信息来解析当前用户的身份,将会调用 DefaultSecurityManager#resolvePrincipals
方法,继续调用 AbstractRememberMeManager#getRememberedPrincipals
方法,如下图:

这个方法就是将 SubjectContext 中的信息转为 PrincipalCollection 的关键方法,也是漏洞触发点。在 try 语句块中共有两个方法,分别是 getRememberedSerializedIdentity
和 convertBytesToPrincipals
方法。
刚才提到,CookieRememberMeManager 对 getRememberedSerializedIdentity
的实现是获取 Cookie 并 Base64 解码,并将解码后的 byte 数组穿入 convertBytesToPrincipals
处理,这个方法执行了两个操作:decrypt
和 deserialize
。

decrypt
是使用 AesCipherService 进行解密。

deserialize
调用 this.serializer#deserialize
方法反序列化解密后的数据。

在 Shiro 中,序列化器的默认实现是 DefaultSerializer,可以看到其 deserialize
方法使用 Java 原生反序列化,使用 ByteArrayInputStream 将 byte 转为 ObjectInputStream ,并调用 readObject
方法执行反序列化操作。

反序列化得到的 PrincipalCollection 会被 set 到 SubjectContext 供后续的校验调用。
以上就是 Shiro 创建 Subject 时执行的逻辑,跟下来后就看到了完整的漏洞触发链:攻击者构造恶意的反序列化数据,使用硬编码的 AES 加密,然后 Base64 编码放在 Cookie 中,即可触发漏洞利用。
小坑
漏洞流程到这里就明白了,接下来就是使用环境内存在的 Gadget 进行攻击了,但是这个有一个小问题。在使用一些 Gadget 如 CC6 时会报错:Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;]
。
很多文章对其描述如下:
Shiro 使用 ClassResolvingObjectInputStream
执行反序列化的操作,这个类重写了 resolveClass
,实际使用 ClassLoader.loadClass()
方式而非 ObjectInputStream 中的 Class.forName()
的方式。而 forName
的方式可以加载任意的数组类型,loadClass
只能加载原生的类型的 Object Array。

在代码审计知识星球中的《Java安全漫谈 – 15.TemplatesImpl在Shiro中的利用.pdf》 及 《Shiro-1-2-4-RememberMe反序列化漏洞分析-CVE-2016-4437.pdf》两篇文章中针对此问题进行了讨论和调试。
其中 @rai4over 师傅指出,ClassLoader.loadClass()
其实也是可以加载数组类型, 而且在 Tomcat 情况下,最终的调用还是使用 Class.forName()
。
P牛没给出调试过程,给出了他的结论:如果反序列化流中包含非 Java 自身的数组,则会出现无法加载类的错误。
总结下来,这可能还是类加载器的问题。网上多篇文章中都给出了此问题的两个解决方案:
- 使用 RMI 中的 Gadget 做跳板,再执行 CC 反序列化链,这样可以加载;
- 改造 CC 链,组合 InvokerTransformer 与 TemplatesImpl,避免使用 Transformer 数组。
这两部分知识在我之前的《Java 反序列化取经路》及《Java RMI 攻击由浅入深》中均有知识铺垫,非常简单,不再重复粘贴代码。
关于此漏洞还有诸多延伸,将在后续章节中进行详解。
漏洞修复
早在 SHIRO-441,就有人提出了硬编码可能导致的安全信息泄露问题,但是官方并未理睬,直到 foxglovesecurity 团队的 @breenmachine 发出了关于 Java 反序列化的文章,漏洞提交者看了这篇文章,发掘了整个流程,将硬编码解密和反序列化结合起来,才引起了官方重视。
Shiro 在 1.2.5 的更新 Commit-4d5bb00 中针对此漏洞进行了修复,描述为:Force RememberMe cipher to be set to survive JVM restart.If the property is not set, a new cipher will be generated.
也就是说,应用程序需要用户手动配置一个 cipherKey,如果不设置,将会生成一个新 key。

通过代码更新可以看出,Shiro 移除了 AbstractRememberMeManager 中的硬编码 key 成员变量 DEFAULT_CIPHER_KEY_BYTES
,在程序初始化时使用了 AesCipherService
生成了新的 key。

这一更新就缓解了硬编码的问题,但是并不代表程序完全安全,因为反序列化流程没变,如果用户自己将 cipherKey 设置为原本硬编码的key,或者比较常见的 key,那程序还是会受到攻击。
CVE-2016-6802
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2016-6802 / CNVD-2016-07814 |
影响版本 | shiro < 1.3.2 |
漏洞描述 | Shiro 使用非根 servlet 上下文路径中存在安全漏洞。远程攻击者通过构造的请求, 利用此漏洞可绕过目标 servlet 过滤器并获取访问权限。 |
漏洞关键字 | 绕过 & Context Path & 非根 & /x/../ |
漏洞补丁 | Commit-b15ab92 |
相关链接 | https://www.cnblogs.com/backlion/p/14055279.html |
漏洞详解
本漏洞类似 CVE-2010-3863,依旧是路径标准化导致的问题,不过之前是在 Request URI 上,本漏洞是在 Context Path 上。
之前提到,Shiro 调用 WebUtils.getPathWithinApplication()
方法获取请求路径。逻辑如下:

其中调用 WebUtils.getContextPath()
方法,获取 javax.servlet.include.context_path
属性或调用 request.getContextPath()
获取 Context 值。并调用 decodeRequestString
进行 URLDecode。

由于获取的 Context Path 没有标准化处理,如果是非常规的路径,例如 CVE-2010-3863 中出现过的 /./
,或者跳跃路径 /su18/../
,都会导致在 StringUtils.startsWithIgnoreCase()
方法判断时失效,直接返回完整的 Request URI 。
这样 Shiro 匹配不到配置路径,就会在某些配置下发生绕过,如下图:

ContextPath
到这里漏洞原理基本说清楚了,但是有一个需要关注的点是,request.getContextPath()
为什么会返回 “/su18/../shiro”,应用程序怎么处理的 Context Path?
这里以 Tomcat 为例,request.getContextPath()
方法的实际实现在 org.apache.catalina.connector.Request
中,这个逻辑其实自己 DEBUG 一下很快就清楚了,我这里简单描述一下其中的逻辑。
方法从 ServletContext 中获取 ContextPath,然后获取 RequestURI。

然后从第二个 “/” 开始,每次截取到下一个 “/”,做路径标准化,对比 ContextPath,直到两者相等,则 substring 到指定位置后返回。

例如,访问 “/su18/../shiro/admin”,而 context path 是 “/shiro”,就会有如下的过程:
- “/su18/” 标准化为-> “/su18” 匹配 “/shiro” 失败;
- “/su18/..” 标准化为-> “/” 匹配 “/shiro” 失败;
- “/su18/../shiro” 标准化为-> “/shiro” 匹配 “/shiro” 成功,于是返回
于是 request.getContextPath()
就返回 “/su18/../shiro” 了。
漏洞修复
Shiro 在 1.3.2 版本的更新 Commit-b15ab92 中针对此漏洞进行了修复。

通过代码可以看出,在 WebUtils.getContextPath
方法进行了更新,使用了修复 CVE-2010-3863 时更新的路径标准化方法 normalize
来处理 Context Path 之后再返回。
本次更新还附带相关的测试文件,里面提供了很多的案例,其实可以用作 fuzz 思路。

但这里提出几个思考:
- shiro 用
request.getContextPath()
获取之后自己做标准化,为什么不直接request.getServletContext().getContextPath()
? - shiro 从
request.getRequestURI()
中获取然后截取,为什么不直接用request.getServletPath()
?
CVE-2019-12422
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2019-12422 / CNVD-2016-07814 / SHIRO-721 |
影响版本 | shiro < 1.4.2 (1.2.5, 1.2.6, 1.3.0, 1.3.1, 1.3.2, 1.4.0-RC2, 1.4.0, 1.4.1) |
漏洞描述 | RememberMe Cookie 默认通过 AES-128-CBC 模式加密,这种加密方式容易受到 Padding Oracle Attack 攻击,攻击者利用有效的 RememberMe Cookie 作为前缀, 然后精心构造 RememberMe Cookie 值来实现反序列化漏洞攻击。 |
漏洞关键字 | 反序列化 & RememberMe & Padding & CBC |
漏洞补丁 | Commit-a801878 |
相关链接 | https://blog.skullsecurity.org/2016/12 https://resources.infosecinstitute.com/topic/padding-oracle-attack-2/ https://codeantenna.com/a/OwWV5Ivtsi |
漏洞详解
本次漏洞实际并不是针对 shiro 代码逻辑的漏洞,而是针对 shiro 使用的 AES-128-CBC 加密模式的攻击,首先了解一下这种加密方式。
AES-128-CBC
AES 全称 Advanced Encryption Standard (高级加密标准),是一种为了取代其前任标准(DES)而成为新标准的对称分组加密算法。这里有几个关键字:
- 对称:所谓对称加密,即使用同一组 key 进行明文和密文的转换。
- 分组加密算法:将明文分成多个等长的模块(block),使用确定的算法和对称密钥对每组分别加密解密。常见的有 ECB、CBC、PCBC、CFB、OFB、CTR 等几种算法。
- 分组长度固定为 128bit 。
- 密钥 key 的长度可以为 128 bit(16字节)、192 bit(24字节)、256 bit(32字节)。根据密钥的长度不同,推荐加密轮数也不同,上述三个密钥长度分别迭代 10/12/14 轮。加密轮数越多,安全性越好,同时也更耗费时间。
因此 AES-128-CBC 模式就代表使用 AES 密钥长度为 128 bit,使用 CBC 分组算法的加密模式。
再来了解一下 CBC,全称 Cipher Block Chaining (密文分组链接模式),简单来说,是一种使用前一个密文组与当前明文组 XOR 后再进行加密的模式。
关于 AES 加解密流程实现可以看这篇文章,关于 CBC 分组的实现可以看这篇文章。这里就不占篇幅描述了。
CBC 模式下,有三种填充方式,用于在分组数据不足时,在结尾进行填充,用于补齐:
- NoPadding:不填充,明文长度必须是 16 Bytes 的倍数。
- PKCS5Padding:以完整字节填充 , 每个填充字节的值是用于填充的字节数 。即要填充 N 个字节 , 每个字节都为 N。
- ISO10126Padding:以随机字节填充 , 最后一个字节为填充字节的个数。
Shiro 中使用的是 PKCS5Padding,也就是说,可能出现的 padding byte 值只可能为:
- 1 个字节的 padding 为 0x01
- 2 个字节的 padding 为 0x02,0x02
- 3 个字节的 padding 为 0x03,0x03,0x03
- 4 个字节的 padding 为 0x04,0x04,0x04,0x04
- …
当待加密的数据长度刚好满足分组长度的倍数时,仍然需要填充一个分组长度,也就是说,明文长度如果是 16n,加密后的数据长度为 16(n+1) 。
Padding Oracle Attack
Padding Oracle Attack 就是针对 CBC 模式分组加密算法的一种攻击手段,可以查看这篇文章学习,英文有困难的小伙伴可以查看这篇文章,说的非常清晰。
这里还是简单的描述一下攻击思路,在加密时,最后一个分组如果长度不够,会进行填充,然后使用倒数第二个分组的密文作为 IV 进行异或,然后进行 AES 加密。
在解密时,先对密文组(CiperText)使用密钥 (Key) 进行 AES 解密,得到一个中间值(MediumValue),然后再异或 IV (也就是上一个密文组) 就会得到这个分组的明文分组(PlainText)。
这个明文分组,是经过 PKCS5Padding 规范填充过的,因此它一定是遵从 PKCS5Padding 的规范的,这个规范就是本次的攻击验证点。
Padding Oracle Attack 就是利用了异或的魅力以及 PKCS5Padding 规范的可穷举性进行的攻击,wikipedia 中给出解释:

这个攻击逻辑我想了小一天,看了 fynch3r 师傅的博客,又咨询了下,最后想通了,这里用比较清晰的话描述出来,供跟我一样密码学和数学基础较差的朋友理解:
- 攻击者修改倒数第二组密文的最后一个字节,发送到服务器,服务器解密后得到 MediumValue,将其与攻击者修改后的倒数第二组密文异或,得到 PlainText,然后对其进行 Padding 校验,此时校验大概率会失败,因为修改过的密文与 MediumValue 异或后不是原本的 Padding 了。
- 此时攻击者遍历修改倒数第二组密文的最后一个字节 ( 0x00 – 0xFF , 最多遍历 255 次 ),使其与 MediumValue 异或,直到最后一个字节异或的结果是 0x01 ,这样得到的 PlainText 是符合 Padding 规范的,攻击者期待程序返回不一样的结果进行判断。这种情况下攻击者可以知道:MediumValue 异或攻击者遍历修改倒数第二组密文的结果的最后一个字节为 0x01 ,根据异或的运算法则,MediumValue 最后一个字节就是 0x01 异或攻击者修改的字节。此时攻击者得到了
MediumValue[8]
的值。此时攻击者知道了MediumValue[8]
的值,还可以知道原倒数第二组密文最后一个字节的值,就可以计算出原PlainText[8]
的值, - 接下来攻击者遍历修改倒数第二组密文的倒数第二个字节,此时攻击者希望异或运算后得到的明文分组的最后两个字节为 0x02 0x02,这样是符合 Padding 规范的,并且由于已经计算了出了
PlainText[8]
的值,因此在这轮遍历中可以用原倒数第二组密文最后一个字节的值异或PlainText[8]
再异或 0x02 作为倒数第二组密文的最后一个字节,因为它与MediumValue[8]
的异或一定为 0x02,攻击者依旧只需要遍历第二组密文的倒数第二个字节即可。 - 依次类推,可以依次计算出最后一组密文中全部的 MediumValue 及 PlainText。
- 舍弃掉最后一组密文,向服务器提交第一组至倒数第二组密文,迭代之前的操作,获得倒数第二组明文。依次规律,直到获得所有分组的明文。
看了全网,发现这部分流程还是 Epicccal 师傅的相关博客写的最为清晰。
至此,攻击者可以在不知道密钥 Key 的情况下得到全部明文的值。但这有两个前提:
- 服务器会对解密结果进行 padding 校验,并且结果可以从响应中进行判断(类似 SQL 盲注)。
- 攻击者已知能正确解密和使用的密文以及初始向量 IV。
CBC Byte-Flipping Attack
到现在已经可以使用 Padding Oracle Attack 在不知道 key 的情况下获取全部明文的值,但这仅仅是信息泄露,能不能进一步篡改信息呢?这里就用到了 CBC 字节翻转攻击。
相关原理可看这篇文章以及这篇文章。这里还是简单描述:通过修改密文进而篡改明文。
在解密时,会使用 MediumValue 与上一组密文进行异或来得到明文,现在知道上一组密文,也知道本组的明文,就能计算出本组的 MediumValue,如果想要异或出不一样的数据,我们只需要篡改上一组的密文,使其跟 MediumValue 能异或出指定的数据即可。
这是一个逆推的过程:
- 获取最后一组密文,由 Padding Oracle Attack 爆破出其 MediumValue ,根据篡改后的明文与 MediumValue 异或,得到前一轮的密文。
- 再使用计算出来的前一轮的密文继续爆破出对应的 MediumValue,再根据篡改后的明文进行异或,再得到前一轮的密文。
- 以此类推到第一组,异或出的值作为起始 IV。
拼接起始 IV 以及全部计算出的每组的密文即可获得一个可以使服务器解密为指定明文数据的密文了。
Attack In Shiro
在了解上面的基础知识后,就很好理解后面的攻击流程了,攻击者通过已知 RememberMe 密文使用 Padding Oracle Attack 爆破和篡改密文,构造可解密的恶意的反序列化数据,触发反序列化漏洞。
之前提到过 Padding Oracle Attack 是利用类似于盲注的思想来判断是否爆破成功的,在校验 Padding 失败时的返回信息应该不同,那 shiro 是否满足这个条件呢?
关注点依旧从 AbstractRememberMeManager#getRememberedPrincipals
中开始,

负责解密的 convertBytesToPrincipals
方法会调用 CipherService 的 decrypt 方法,接下来的调用链大概如下:
org.apache.shiro.crypto.JcaCipherService#decrypt()
javax.crypto.Cipher#doFinal()
com.sun.crypto.provider.AESCipher#engineDoFinal()
com.sun.crypto.provider.CipherCore#doFinal()
com.sun.crypto.provider.CipherCore#fillOutputBuffer()
com.sun.crypto.provider.CipherCore#unpad()
com.sun.crypto.provider.PKCS5Padding#unpad()
其中 PKCS5Padding#unpad
方法中会判断数据是否符合填充格式,如果不符合,将会返回 -1。

CipherCore#doFinal
方法根据返回结果抛出 BadPaddingException 异常。

被 JcaCipherService#crypt
方法 catch 住并抛出 CryptoException 异常。

被 AbstractRememberMeManager#getRememberedPrincipals
方法 catch 住,并调用 onRememberedPrincipalFailure
处理。

解析身份信息失败,将会调用 forgetIdentity 方法移除 rememberMe cookie。

并为响应 header 添加 deleteMe 头部。

具体逻辑如下:

由此可见,只要 padding 错误,服务端就会返回一个 cookie: rememberMe=deleteMe;
,攻击者可以借由此特征进行 Padding Oracle Attack。
漏洞复现
到此全部基础知识补充完毕,可以开始真实的攻击了。在这里由于时间、精力和技术的限制,就不再自行实现相关逻辑,直接使用 longofo 师傅的项目。
首先获取一个有效的 rememberMe 值,其次生成一个反序列化利用的 payload,然后使用如下参数执行攻击。
java -jar PaddingOracleAttack.jar http://127.0.0.1:8080/shiro/index "F5Dktxan5VMiWyF...MM" 16 /Users/phoebe/IdeaProjects/ysoserial-su18/URLDNS.bin
将生成的 payload 放在 rememberMe 中发送至服务器。

服务端成功解密并反序列化数据。

反序列化的执行逻辑与 SHIRO-550 一致,这里不再重复。
问题
这个漏洞已经完全走通了,但是存在一个严峻的问题是:
每个字节的爆破最多情况下需要 255 次,一个加密组 16 个字节,最大可能需要 255*16=4080 次爆破。
根据不通长度的 payload 将生成若干个加密组区块,每个加密块都需要进行上述爆破流程。在复杂的 payload 下,爆破生成完整的篡改包需要发出的请求数将会是天文数字。
在测试时,使用 URLDNS 进行测试,根据 payload 长度,共分了 21 个数据分组,生成篡改包共发送了 42051 个请求包,如下图。

如果是使用 CC6 配合 TemplatesImpl 的 gadget,需要爆破的密文组将达到惊人的 150 个,按平均每组发送 2000 个爆破包来算(非常保守的估计),想要成功执行一次反序列化,需要发送 30W 个请求!
因此,在真实情况下,执行此漏洞的攻击代价过大,最好的方式也只能是结合 RMI 的两条链曲线救国,将真正的执行点放在其他位置,但即使是这样,也要发送几万条的请求,实在是过于离谱。
漏洞修复
在 1.4.2 版本的更新 Commit-a801878 中针对此漏洞进行了修复 ,在父级类 JcaCipherService 中抽象出了一个 createParameterSpec()
方法返回加密算法对应的类。

并在 AesCipherService 中重写了这个方法,默认使用 GCM 加密模式,避免此类攻击。

CVE-2020-1957
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2020-1957 / CNVD-2020-20984 / SHIRO-682 |
影响版本 | shiro < 1.5.2 |
漏洞描述 | Spring Boot 中使用 Apache Shiro 进行身份验证、权限控制时,可以精心构造恶意的URL 利用 Shiro 和 SpringBoot 对 URL 的处理的差异化,可以绕过 Shiro 对 SpringBoot 中的 Servlet 的权限控制,越权并实现未授权访问。 |
漏洞关键字 | SpringBoot & 差异化处理 & / & 绕过 |
漏洞补丁 | Commit-589f10d && Commit-9762f97 && Commit-3708d79 |
相关链接 | SHIRO-742 https://www.openwall.com/lists/oss-security/2020/03/23/2 CVE-2020-2957 -> ? |
漏洞详解
本 CVE 其实包含了几个版本的修复与绕过过程,这也导致了在网上搜索本 CVE 时可能得到不同 POC 的漏洞复现文章,这里就从头开始说一下。
SHIRO-682
本漏洞起源于 SHIRO-682,Issues 描述了在 SpingWeb 中处理 requestURI 与 shiro 中匹配鉴权路径差异导致的绕过问题:在 Spring 中,/resource/menus
与 /resource/menus/
都可以访问资源,但是在 shiro 中,这两个路径是成功匹配的,所以在 Spring 集成 shiro 时,只需要在访问路径后添加 “/” 就存在绕过权限校验的可能。
接下来简单复现一下,如下图配置请求路径 “/admin/list” 需要认证和授权。

正常访问会提示跳转到登陆页面。

此时在请求路径后添加 “/”,即 “/admin/list/”,即可绕过权限校验

这个漏洞的原理在前面 Issues 的描述中已经说的很明白了,其实就是 spring 在分发请求时,会从 DispatcherServlet#handlerMappings
找到能匹配路径的 Handler,会遍历匹配路径,负责匹配的 PathPattern#match
方法对 “/admin/list/” 和 “/admin/list” 的匹配会返回 true。

而 shiro 用来匹配的 PathMatchingFilterChainResolver#pathMatches
则会返回 false。

这一差异导致了校验的绕过。
绕过
除了上面的漏洞,本 CVE 通报版本号内还存在一个另一个绕过。利用的是 shiro 和 spring 对 url 中的 “;” 处理的差别来绕过校验。
还是先来复现一下,直接扔截图。

很显然,绕过的原理就是访问 /aaaadawdadaws;/..;wdadwadadw/;awdwadwa/audit/list
这个请求的时候会被 shiro 和 spring 解析成不同的结果。
先来看下 shiro,之前提到过,shiro 会用自己处理过的 RequestURI 和配置的路径进行匹配,具体的方法就是 WebUtils#getRequestUri
,方法先调用 decodeAndCleanUriString
方法处理请求路径,再调用 normalize 方法标准化路径。decodeAndCleanUriString
方法逻辑如下,可以看到,对 URL 中存在 “;” 的处理是直接截断后面的内容。

那 Spring 是怎么处理的呢?方法是 UrlPathHelper#decodeAndCleanUriString
,方法名也叫 decodeAndCleanUriString
,你说巧不巧?其实一点也不巧,这分明就是 shiro 抄 spring 的作业。
方法里一次执行了 3 个动作:removeSemicolonContent 移除分号,decodeRequestString 解码,getSanitizedPath 清理路径,具体描述如下图:

其中出现差异的点就在于 UrlPathHelper#removeSemicolonContent
,逻辑如下图:

可以看到,spring 处理了每个 / / 之间的分号,均把 “;” 及之后的内容截取掉了。所以当请求 /aaaadawdadaws;/..;wdadwadadw/;awdwadwa/audit/list
进入到 UrlPathHelper#decodeAndCleanUriString
方法时,会逐渐被处理:
- removeSemicolonContent:”/aaaadawdadaws/..//audit/list”
- decodeRequestString:”/aaaadawdadaws/..//audit/list”
- getSanitizedPath:”/aaaadawdadaws/../audit/list”
这样再标准化就会成为正常的 “/audit/list”。
这种思路是哪里来的呢?其实又是抄了 Tomcat 的处理思想,处理逻辑位于 org.apache.catalina.connector.CoyoteAdapter#parsePathParameters
如下图

也就说,在 Tomcat 的实现下,对于访问 URL 为 “/aaaadawdadaws;/..;wdadwadadw/;awdwadwa/audit/list” 的请求,使用 request.getServletPath()
就会返回 “/audit/list”。
而由于 spring 内嵌 tomcat ,又在处理时借鉴了它的思路,所以导致 UrlPathHelper#getPathWithinServletMapping
方法其实无论如何都会返回经过上述处理逻辑过后的路径,也就是 “/audit/list”。
了解了这个处理机制后,这个路径就可以被花里胡哨的改为:
http://127.0.0.1:8080/123;/..;345/;../.;/su18/..;/;/;///////;/;/;awdwadwa/audit/list
依然可以绕过校验:

经测试,上面这个 payload 只能在较低版本的 Spring Boot 上使用。为什么呢?直接引用 Ruil1n 师傅的原文:
当 Spring Boot 版本在小于等于 2.3.0.RELEASE 的情况下,alwaysUseFullPath 为默认值 false,这会使得其获取 ServletPath ,所以在路由匹配时相当于会进行路径标准化包括对 %2e 解码以及处理跨目录,这可能导致身份验证绕过。而反过来由于高版本将 alwaysUseFullPath 自动配置成了 true 从而开启全路径,又可能导致一些安全问题。
针对这方面的内容,截止至本文发出前,先知上有师傅发出了tomcat容器url解析特性研究,对其中的相关内容进行了详述,可移步观看。
在高版本上不处理跨目录,就只能借助 shiro 一些配置问题尝试绕过:比如应用程序配置了访问路径 “/audit/**” 为 anon,但是指定了其中的一个 “/audit/list” 为 authc。这时在不跳目录的情况下,可以使用如下请求绕过:
http://127.0.0.1:8080/audit//;aaaa/;...///////;/;/;awdwadwa/list
漏洞修复
首先是针对 SHIRO-682 的修复,共提交了两次,第一次为 Commit-589f10d ,如下图,可以看到是在 PathMatchingFilter#pathsMatch
方法中添加了对访问路径后缀为 “/” 的支持。

同时在 PathMatchingFilterChainResolver#getChain
也添加了同样的逻辑。

第二次是 Commit-9762f97,是修复由于上一次提交,导致访问路径为 “/” 时抛出的异常。可以看到除了 endsWith
还添加了 equals
的判断。

然后是对使用 “;” 绕过的修复 Commit-3708d79, 可以看到 shiro 不再使用 request.getRequestURI()
来获取用户妖魔鬼怪的请求路径,而是使用 request.getContextPath()
、request.getServletPath()
、request.getPathInfo()
进行拼接,直接获取中间件处理后的内容。

CVE-2020-11989
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2020-11989 / SHIRO-782 |
影响版本 | shiro < 1.5.3 |
漏洞描述 | 由安全研究员 Ruilin 以及淚笑发现在 Apache Shiro 1.5.3 之前的版本, 将 Apache Shiro 与 Spring 动态控制器一起使用时,特制请求可能会导致身份验证绕过。 |
漏洞关键字 | Spring & 双重编码 & %25%32%66 & 绕过 & context-path & /;/ |
漏洞补丁 | Commit-01887f6 |
相关链接 | https://xlab.tencent.com/cn/2020/06/30/xlab-20-002/ https://mp.weixin.qq.com/s/yb6Tb7zSTKKmBlcNVz0MBA |
漏洞详解
此版本漏洞依旧是存在了两种绕过的手段,也分别由报送漏洞的腾讯玄武实验室和边界无限给出了漏洞利用的细节,这里还是依次来看一下。
AntPathMatcher 绕过
根据腾讯玄武实验室官方给出的漏洞细节文章,本漏洞是需要几个利用条件的,接下来看一下具体的细节。
Shiro 支持 Ant 风格的路径表达式配置。ANT 通配符有 3 种,如下表:
通配符 | 说明 |
---|---|
? | 匹配任何单字符 |
* | 匹配0或者任意数量的字符 |
** | 匹配0或者更多的目录 |
在之前的测试和使用中,常见的就是 /**
之类的配置,匹配路径下的全部访问请求,包括子目录及后面的请求,如:/admin/**
可以匹配 /admin/list
以及 /admin/get/id/2
等请求。
另外一个类似的配置是 /*
,单个 *
不能跨目录,只能在两个 /
之间匹配任意数量的字符,如 /admin/*
可以匹配 /admin/list
但是不能匹配 /admin/get/id/2
。
Shiro 对于 Ant 风格路径表达式解析的支持位于 AntPathMatcher#doMatch
方法中,这里简单说一下其中的逻辑:
首先判断配置的表达式 pattern 和访问路径 path 起始是否均为 /
或均不是,如果不同则直接返回 false。

然后将 pattern 和 path 均切分为 String 类型的数组。

然后开始循环判断 pattern 和 path 对应位置的配置和路径是否有匹配,判断使用 AntPathMatcher#matchStrings
方法。

AntPathMatcher#matchStrings
方法又把字符拆分成 char 数组,来进行匹配尝试,并支持 *
以及 ?
类型的通配符的匹配。

本次漏洞涉及到的配置则是使用 *
配置。再再次重温一下 shiro 的处理逻辑:
WebUtils#getRequestUri
方法使用 request.getContextPath()/request.getServletPath()/request.getPathInfo()
获取用户请求路径,然后调用 decodeAndCleanUriString
方法解码并取出 ;
之后的内容,然后调用 normalize 标准化路径。

decodeAndCleanUriString
方法逻辑之前贴过,这里再贴一次。

而漏洞就出在此逻辑处,各位看官集中注意力,我来描述一下:
- 以前的 shiro 使用
request.getRequestURI()
获取用户请求路径,并自行处理,此时 shiro 默认Servlet 容器(中间件)不会对路径进行 URL 解码操作,通过其注释可以看到;

- 在 1.5.2 版本的 shiro 更新中,为了修复 CVE-2020-1957 ,将
request.getRequestURI()
置换为了valueOrEmpty(request.getContextPath()) + "/" + valueOrEmpty(request.getServletPath()) + valueOrEmpty(request.getPathInfo());
,而对于request.getContextPath()
以及request.getPathInfo()
,以 Tomcat 为例的中间件是会对其进行 URL 解码操作的,此时 shiro 再进行decodeAndCleanUriString
,就相当于进行了两次的 URL 解码,而与之后的 Spring 的相关处理产生了差异。
这其中细节,可以查看 mi1k7ea 师傅发表在先知上的文章,我这里截取其中的一小段。

至此已经发现了 shiro 中的路径处理差异问题,由于 shiro 会二次解码路径,因此 %25%32%66
将会被 shiro 解码为 /
,而如果只解码一次, %25%32%66
只会被处理成 %2f
。
此时如果使用了单个 “*” 的通配符,将产生差异化问题,例如如下配置,配置了 /audit/*
:

此时访问 /audit/list
,/audit/aaa
之类的请求,都会被 shiro 拦截,需要进行权限校验。
但是如果访问 /audit/aa%25%32%66a
,在 shiro 处理时,会将其处理为 /audit/aa/a
,此路径并不能被 /audit/*
配置项匹配到,因此会绕过 shiro 校验。而在后续 spring 逻辑中会处理成 /audit/aa%2fa
,可能会绕过请求。
找到了差异点,接下来就要找场景了,Ruil1n 师傅找到了当 Spring 在参数中使用 PathVariable
注解从 RequestMapping 中的占位符中取数据的场景,可以满足上面的情况,如下图:

漏洞复现如下,正常访问:/audit/aaaa
会跳转至登录页面:

使用 %25%32%66
绕过,可以发现绕过:

这里还有一个限制,由 PathVariable 注解的参数只能是 String 类型,如果是其他类型的参数,将会由于类型不匹配而无法找到对应的处理方法。
ContextPath 绕过
这个绕过实际上是对上一个 CVE 思路上的延伸,在 CVE-2020-1957 中,借助了 shiro 和 spring 在获取 requestURI 时对 ;
的处理差异,以及 /../
在路径标准化中的应用,进行了权限绕过。
而这次的绕过,则是在 ContextPath 之前使用 /;/
来绕过,访问如:/;/spring/admin/aaa
路径,根据已经了解到的知识:
- shiro 会截取掉
;
之后的路径,按照/
来匹配; - spring 会把路径标准化为
/spring/admin/aaa
来匹配。
这就产生了 shiro 鉴权的路径和 spring 处理的路径不同造成的绕过。
淚笑提供了他的漏洞环境。复现如下:

同样,上面这个 payload 只能在较低版本的 Spring Boot 上使用,原因与之前提到过的一致。
漏洞修复
Shiro 在 Commit-01887f6 中提交了针对上述两个绕过的更新。
首先 shiro 回退了 WebUtils#getRequestUri
的代码,并将其标记为 @Deprecated
。并建议使用 getPathWithinApplication()
方法获取路径减去上下文路径,或直接调用 HttpServletRequest.getRequestURI()
方法获取。

其次是在 WebUtils#getPathWithinApplication
方法,修改了使用 RequestUri 去除 ContextPath 的减法思路,改为使用 servletPath + pathInfo 的加法思路。加法过后使用 removeSemicolon
方法处理分号,normalize
方法标准化路径。

getServletPath
和 getPathInfo
方法逻辑如下:

更新后,shiro 不再处理 contextPath,不会导致绕过,同时也避免了二次 URL 解码的问题。
CVE-2020-13933
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2020-13933 / CNVD-2020-46579 |
影响版本 | shiro < 1.6.0 |
漏洞描述 | Apache Shiro 由于处理身份验证请求时存在权限绕过漏洞,远程攻击者可以发送特制的 HTTP请求,绕过身份验证过程并获得对应用程序的未授权访问。 |
漏洞关键字 | Spring & 顺序 & %3b & 绕过 |
漏洞补丁 | Commit-dc194fc |
相关链接 | https://xz.aliyun.com/t/8223 |
漏洞详解
这个 CVE 实际上是对上一个 CVE 中 AntPathMatcher 绕过方式的再次绕过。
在上一个 CVE 的修复补丁中提到,Shiro 使用了 servletPath + pathInfo 的加法思路获取访问 URI。获取两者值的方法均为从 attribute 中获得对应的值,如果为空则调用 request.getXX
对应的方法进行获取,加法过后使用 removeSemicolon
方法处理分号,normalize
方法标准化路径。之前也提到过,request.getXX
方法,会进行 URL 解码操作。
这里需要注意的是处理顺序的问题,按照上述逻辑,shiro 对于路径的处理,会先 URL 解码,再处理分号,然后标准化路径。
这个顺序将会与 Spring 及 Tomcat 产生差异,之前提到过,在 UrlPathHelper#decodeAndCleanUriString
方法中,是后两者是先处理分号,再 URL 解码,然后标准化路径。
这一差异将会导致,当请求中出现了 ;
的 URL 编码 %3b
时,处理顺序的不同将会带来结果不同导致绕过:
- shiro 会 url 解码成
;
,然后截断后面的内容,进行匹配,例如/audit/aaa%3baaa
->/audit/aaa
。 - spring & tomcat 会处理成
/audit/aaa;aaa
。
两者处理后的结果不同,就造成了绕过。差异点找到了,接下来就是场景,也同样依赖 PathVariable
注解 String 类型的参数。
这里有一个点是,对于使用了 /audit/*
配置的鉴权,无法是匹配 /audit/
的。
因此,对于配置了 /audit/*
的鉴权,可以使用 /audit/%3baaa
来使 shiro 处理成 /audit/
,并结合在 spring 中 PathVariable 的场景即可实现绕过。
漏洞复现如下:

漏洞修复
本次漏洞修复位于 Commit-dc194fc 中,在这此更新中,shiro 没有改动现有的处理逻辑,而是选择了使用全局过滤和处理的方式。
Shiro 创建了一个 global 的 filter:InvalidRequestFilter
,这个类继承了 AccessControlFilter
。用来过滤和阻断有危害的请求,会返回 400 状态码,其中包括:
- 带有分号的请求;
- 带有反斜线的请求;
- 非 ASCII 字符。
这个类是根据 spring-security 中的 StrictHttpFirewall 类编写而来。

其中关键的 isAccessAllowed
方法会进行逐个校验。
shiro 将 InvalidRequestFilter
配置在 Global Filter 中。

并使其默认匹配 “/**”,使其可以全局匹配进行过滤校验。

CVE-2020-17510
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2020-17510 / CNVD-2020-60318 |
影响版本 | shiro < 1.7.0 |
漏洞描述 | Apache Shiro 由于处理身份验证请求时存在权限绕过漏洞,远程攻击者可以发送特制的 HTTP请求,绕过身份验证过程并获得对应用程序的未授权访问。 |
漏洞关键字 | Spring & 编码 & %2e & 绕过 & /%2e%2e/ |
漏洞补丁 | Commit-6acaaee |
相关链接 | https://lists.apache.org/thread/12bn9ysx6ogm830stywro4pkoq8dxzfk |
漏洞详解
本漏洞还是对 AntPathMatcher 的继续绕过。之前已经尝试了 ;
的 URL 编码,/
的双重 URL 编码的绕过,都是因为 Shiro 先 url 解码再标准化和处理的逻辑与 Spring 不同导致的。
那还有什么字符的 URL 编码可能导致问题呢?常见的 URL 中还有什么字符能用呢?答案就是 .
,.
的 URL 编码为 %2e
。
当一个 %2e
出现在请求中时,会发生什么事呢?很显然,shiro 会将其当做 .
处理,而 Spring 会将其当做字符 %2e
处理。
此时如果 %2e
出现的位置正确,就可以在 shiro 处理后消失,造成差异,例如访问:”/audit/%2e/”:
- Shiro url decode:”/audit/./”
- Shiro 标准化路径:”/audit/”
- Spring 标准化路径:”/audit/%2e/”
- Spring url decode:”/audit/.”
由此可见,Shiro 匹配的路径和 Spring 匹配的路径相差了一个字符 “.”,将造成绕过。此时依旧借助单个 “*” 的通配符以及 PathVariable
注解 String 类型的参数的场景触发漏洞。

可以使用的 payload 包括:
/%2e
/%2e/
/%2e%2e
/%2e%2e/
因为上面的写法都会被 shiro 的标准化路径处理掉,并且同时能被 PathVariable
注解 String 类型的参数匹配到。
漏洞修复
Shiro 在 Commit-6acaaee 中提交了本次漏洞的修复。
在本次修复中可以看到,Shiro 的思路再次转变,不再按照 Spring 和 Tomcat 改自己的处理代码,也不再给自己加代码来适配 Spring,而是创建了 UrlPathHelper 的子类 ShiroUrlPathHelper,并重写了 getPathWithinApplication
和 getPathWithinServletMapping
两个方法,全部使用 Shiro 自己的逻辑 WebUtils#getPathWithinApplication
进行返回。

在之前的分析中我们知道,Spring 与 Shiro 处理逻辑之间的差异就在这个位置,而现在 Shiro 直接把代码逻辑重写,通过注入自己的代码来修改 Spring 的相关逻辑,用来保证二者没有差异。究竟是怎么注入的呢?在配置类中 import 了 ShiroRequestMappingConfig
类。

ShiroRequestMappingConfig
类会向 RequestMappingHandlerMapping#urlPathHelper
设置为 ShiroUrlPathHelper
。

设置后,Spring 匹配 handler 时获取路径的逻辑就会使用 Shiro 提供的逻辑,保持了二者逻辑的一致。从而避免了绕过的情况。
注意
这里需要注意的是,Shiro 官方对这个漏洞的修复非常坑,根据官方给出的信息,Shiro 将修复放在了 shiro-spring-boot-web-starter
包中,也就是使用了 shiro-spring-boot-web-starter
进行配置的项目,升级版本才会使防御代码生效,才会注入 ShiroUrlPathHelper 。
如果你没有使用shiro-spring-boot-web-starter
自动配置,而是引入 shiro-spring
自己进行注入 Bean,单纯的升级版本是无法防御本次 CVE 的,需要:
如果不配置,将无法有效防御此 CVE。
绕过
这个修复在当时来看,如果配置正确,防御能力是 OK 的,整个思路都没问题,但是随着 Spring 自身代码的迭代,却又将安全问题暴露了出来。在高版本的 Spring 中,由于 alwaysUseFullPath
默认为 true ,导致应用程序使用 UrlPathHelper.defaultInstance
来处理,而不是 Shiro 实现的 ShiroUrlPathHelper
来处理。

这样就导致这个修复补丁又被完美的绕过了。

CVE-2020-17523
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2020-17523 / CNVD-2021-09492 |
影响版本 | shiro < 1.7.1 |
漏洞描述 | Apache Shiro 由于处理身份验证请求时存在权限绕过漏洞,远程攻击者可以发送特制的 HTTP请求,绕过身份验证过程并获得对应用程序的未授权访问。 |
漏洞关键字 | Spring & trim & %20 & 绕过 & /%20%20/ |
漏洞补丁 | Commit-ab1ea4a |
相关链接 | https://www.anquanke.com/post/id/230935 https://www.eso.org/~ndelmott/url_encode.html |
漏洞详解
继续绕过…
在使用 .
、/
、;
的 URL 编码绕过之后,这次使用的是空格的 URL 编码:%20
。
之前讲过,在匹配访问路径与配置鉴权路径时,在 AntPathMatcher#doMatch
方法中,首先会调用 org.apache.shiro.util.StringUtils#tokenizeToStringArray
方法将 pattern 以及 path 处理成 String 数组,再进行比对。

这个方法会继续调用有四个参数的重写方法,并且后两个参数的值均为 true。其实这部分也是抄的 spring 的代码。

可以看到后两个布尔类型参数的意义是对 StringTokenizer 结果的处理的标志 flag,代表是否对 token 进行 trim 操作,以及是否忽略空的 token。

因此,在被 WebUtils#getPathWithinApplication
方法处理过的 URI,再与配置路径匹配时,又会处理空格。

因此对于 “/audit/%20” 这种访问,可以理解为会被 shiro 处理成 “/audit/” 这种格式去匹配。
而 Spring 的处理逻辑,在配置了 CVE-2020-17510 的安全补丁后,虽然与 shiro 保持了一致,但是在匹配 handler 时并没有空格的处理,因此可以继续以字符串的方式匹配。
依旧是依赖单个 “*” 的通配符以及 PathVariable
注解 String 类型的参数的场景触发漏洞。复现如下,%20
随便加。

由于之前的安全修复,URL 中的非 ASCII 字符会被 filter 干掉,因此,我 FUZZ 了 %00-ff 的全部字符,发现只有 %20 能用。

漏洞修复
Shiro 在 Commit-ab1ea4a 中提交了本次漏洞的修复。
可以看到是指定了 StringUtils#tokenizeToStringArray
方法的第三个参数 trimTokens 为 false,也就是说不再去除空格,从而消除了本次漏洞的影响。

其实即使不报安全漏洞, shiro 也应该修复这个逻辑,因为 spring 本身可以支持以空格作为 RequestMapping。

而 shiro 对其处理逻辑则有问题,配置后访问将不生效。

如下:

CVE-2021-41303
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2021-41303 / SHIRO-825 |
影响版本 | shiro < 1.8.0 |
漏洞描述 | Apache Shiro 与 Spring Boot 一起使用时,远程攻击者可以发送特制的 HTTP 请求, 绕过身份验证过程并获得对应用程序的未授权访问。 |
漏洞关键字 | Spring & 回退 & /aaa/*/ & 绕过 |
漏洞补丁 | Commit-4a20bf0 |
相关链接 | https://threedr3am.github.io/ |
漏洞详解
在上一个版本的更新中,除了安全修复,还更新了几个逻辑,来优化对路径末尾 “/” 的情况的处理。
第一是匹配路径的方法 PathMatchingFilter#pathsMatch
,在曾经 SHIRO-682 的更新中针对这个方法进行了修改,为了兼容 Spring 对访问路径最后一个 “/” 的支持。

在本次版本更新中,添加了一层判断逻辑,即先使用原始请求判断,如果没有匹配成功,再使用去掉 “/” 的路径尝试匹配。
第二是在 PathMatchingFilterChainResolver
中新增了一个 removeTrailingSlash
方法,用来去除请求路径中的最后的 “/”。

并在 getChain
方法中更改逻辑,依旧是先使用原来的请求匹配,匹配不到再使用去除请求路径之后的 “/” 来匹配。

原本的逻辑是,拿到 URI ,直接判断最后是不是 “/”,如果是直接去掉,然后匹配和处理,但改过之后,直接拿过来匹配,如果没匹配到,再尝试去掉 “/” 在匹配,这种情况下,对于带 “/” 的请求将会匹配两次。
不但逻辑复杂了,而且还写出了 BUG。在 else 语句块中,没有将 pathPattern 给到 filterChainManager#proxy
方法,反而是将用户可控的 requestURINoTrailingSlash 给了进去。
这为什么会产生漏洞呢?这一切先从一个 BUG 说起:SHIRO-825。首先来复现一下这个 ISSUES ,我们配置如下,同样是使用单个 “*” 匹配:
chainDefinition.addPathDefinition("/audit/list", "authc");
chainDefinition.addPathDefinition("/audit/*", "anon");
可以看到,/audit/
路径下只有 list 是需要鉴权的,其他不需要。Controller 代码如下:
@Controller
@RequestMapping(value = "/audit")
public class AuditController {
@GetMapping(value = "/list")
public void list(HttpServletResponse response) throws IOException {
response.getWriter().println("you have to be auditor to view this page");
}
@GetMapping(value = "/{name}")
public void list(@PathVariable String name, HttpServletResponse response) throws IOException {
response.getWriter().println("no need auth to see this page:" + name);
}
}
此时访问 “/audit/aaa” 正常:

但是访问 “/audit/aaa/” 报错:

原因就是,shiro 会用处理过的用户请求路径去配置文件里找对应的路径,自然找不到就抛异常的。

那这个 BUG 是如何延伸成为漏洞的呢?不难想到,如果 shiro 在配置文件中找到了这个路径,那逻辑就正常了。我们再来配置一下场景,现在改为如下配置:
chainDefinition.addPathDefinition("/audit/*", "authc");
chainDefinition.addPathDefinition("/audit/list", "anon");
现在的逻辑是,配置了 /audit/*
需要认证,而 /audit/list
不需要认证,注意配置的顺序,正常逻辑下,对于 /audit/list
对应的路径,是需要鉴权的,因为他会被 /audit/*
匹配到,但是 /audit/*
不能匹配 /audit/list/
,会去掉 “/” 进行匹配,能匹配到,且在后续的逻辑中也可以找到对应的路径,就可以绕过鉴权。

漏洞修复
Shiro 在 Commit-4a20bf0 中修复了此问题。可以看到修改后正确的传入了 pathPattern。

思考
本漏洞的分析是参考了 threedr3am 师傅的博客,但存在几个疑问:
- 本 CVE 在 CVSS 3.0 获得了 9.8 的评分,CVSS 2.0 获得了 7.5 的评分,但上面的漏洞场景似乎限制很大,给不到高危。
- ISSUES 的报送者是报送 BUG,并非安全风险,而官方的通告又致谢了另外一个安全从业者。
- 我翻了所有的更新代码,确实没找到其他类似漏洞修复的地方,因为 shiro 一般修绕过的时候都会给出新的 testcase,确实没找到别的。
转载请注明出处及链接