Apache Shiro 组件反序列化漏洞分析

本文主要介绍Shiro反序列化漏洞,包括Shiro - 550和Shiro - 721。Shiro为保持用户登录状态,将持久化信息加密存于Cookie的rememberMe字段。在Shiro 1.2.4版本前有默认固定加密Key,攻击者可伪造Cookie触发漏洞。还分析了漏洞产生原因、加密解密过程及1.2.5版本修复情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

它的原理比较简单:为了让浏览器或服务器重启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe字段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞。

Shiro反序列化漏洞目前为止有两个,Shiro-550(Apache Shiro < 1.2.5)和Shiro-721( Apache Shiro < 1.4.2 )。这两个漏洞主要区别在于Shiro550使用已知密钥撞,后者Shiro721是使用登录后rememberMe={value}去爆破正确的key值进而反序列化,对比Shiro550条件只要有足够密钥库(条件比较低)、Shiro721需要登录(要求比较高鸡肋)。

Apache Shiro < 1.4.2默认使用AES/CBC/PKCS5Padding模式

Apache Shiro >= 1.4.2默认使用AES/GCM/PKCS5Padding模式

shiro反序列化漏洞成因

概述

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

它的原理比较简单:为了让浏览器或服务器重启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe字段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞。

Shiro反序列化漏洞目前为止有两个,Shiro-550(Apache Shiro < 1.2.5)和Shiro-721( Apache Shiro < 1.4.2 )。这两个漏洞主要区别在于Shiro550使用已知密钥撞,后者Shiro721是使用登录后rememberMe={value}去爆破正确的key值进而反序列化,对比Shiro550条件只要有足够密钥库(条件比较低)、Shiro721需要登录(要求比较高鸡肋)。

Apache Shiro < 1.4.2默认使用AES/CBC/PKCS5Padding模式

Apache Shiro >= 1.4.2默认使用AES/GCM/PKCS5Padding模式

Shiro-550:Hard Code->Deserialize->RCE

Shiro 550 反序列化漏洞存在版本:

shiro <1.2.4,产生原因是因为shiro接受了Cookie里面rememberMe的值,然后去进行Base64解密后,再使用aes密钥解密后的数据,进行反序列化。

这个aes密钥是硬编码(简称写死),也就是他密钥是写死在jar包里面的,众所周知AES 是对称加密,即加密密钥也同样是解密密钥,那如果我们能知道了这个密钥就可以伪造恶意cookie

接下来我们从Cookie的加密和解密过程来了解shiro-550

Cookie加密过程

直接来看shiro的CookieRememberMeManager在org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity里面,存在一个将serialized数据Base64加密然后作为Cookie返回的行为

我们看下哪些地方调用了这个方法,狂摁Ctrl+B:

 org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity<-
 org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity<-
 org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity(重载)<-
 org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin

看到这个函数名都知道是登陆成功调用的,如果继续跟下去的话,会有:

 org.apache.shiro.mgt.DefaultSecurityManager#rememberMeSuccessfulLogin <-
 org.apache.shiro.mgt.DefaultSecurityManager#onSuccessfulLogin<-
 org.apache.shiro.mgt.DefaultSecurityManager#login<-
 ……

会追溯到Filter之类的,大概就是:

登陆->登陆成功->设置Base64编码后的AES加密的Cookie

在onSuccessfulLogin方法这里下个断点

在调用rememberIdentity之前先调用isRememberMe判断了用户是否选择了RememberMe选项,如果选了进入rememberIdentity方法

这个方法先创建一个PrincipalCollection对象,包含了登录信息。

随后进入rememberIdentity方法

这个方法调用convertPrincipalsToBytes把序列化后的PrincipalCollection对象加密,然后返回

而这个seriallize方法,调用org.apache.shiro.mgt.AbstractRememberMeManager#getEncryptionCipherKey去获取加密的key

跟进,发现直接返回了一个属性

转到定义,这个属性貌似是预先定义好的,虽然没看出究竟是哪里定义的,不过我们可以看到一个叫做DEFAULT_CIPHER_KEY_BYTES的东西,这个就是传说中的硬编码的shirokey

之后就是调用rememberSerializedIdentity返回base64加密的cookie了。

接下来康康解密过程:

Cookie解密过程

我们其实可以猜测,加密解密的功能实际上都是由这个

org.apache.shiro.web.mgt.CookieRememberMeManager类来实现的,在这个类里面四处找一找,可以找到getRememberedSerializedIdentity方法里面有一行:

这个很像获取Cookie然后去读取值的操作,在这里下个断点,带着Cookie访问服务,果然就断下来了

单步跟进,发现他获取到了我们的Cookie:

随后判断了一下我们Cookie的值是不是等于DELETED_COOKIE_VALUE (deleteMe),如果不是则进行decode并且返回:

返回到了这里:

并且调用convertBytesToPrincipals(这个函数名字是不是很熟悉?),将Cookie的结果转化为凭据(PrincipalCollection对象)

因为之前加密过程调用convertPrincipalsToBytes,是一个序列化过程,那这里显然就是一个反序列化过程,跟进:

解密,而后反序列化;

跟进,触发点在

org.apache.shiro.io.DefaultSerializer#deserialize

1.2.5 版本修复

修改了org.apache.shiro.mgt.AbstractRememberMeManager的硬编码方式,并且去掉了默认key,采用随机生成的shiro AES key

但是这个key是可以自定义的:

private static final String ENCRYPTION_KEY = "3AvVhmFLUs0KTA3Kprsdag==";
public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememMeCookie());
        // remeberMe cookie 加密的密钥 各个项目不一样 默认AES算法 密钥长度(128 256 512)
        cookieRememberMeManager.setCipherKey(Base64.decode(ENCRYPTION_KEY));
        return cookieRememberMeManager;
}

或者:

spring-shiro.xml

在安全管理器SecurityManager中加入rememberMeManager;

添加rememberMeManager,调用getCipherKey()随机生成密钥。

理论上只要AES加密钥泄露,都会导致反序列化漏洞,也就是说,只要你硬编码,就有可能有爆破的风险

Shiro-721:Padding Oracle Attack->Shiro AES key->shiro550

这个就不是重点了,shiro721本来利用需要先登陆获得有效的rememberMe={value}去爆破正确的key值进而反序列化,利用十分鸡肋。

关于Padding Oracle Attack看这篇:

padding oracle和cbc翻转攻击

大概过程是这样:

比如我们的明文为admin
则需要被填充为 admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b
一共11个\x0b
如果我们输入一个错误的iv,依旧是可以解密的,但是middle和我们输入的iv经过异或后得到的填充值可能出现错误
比如本来应该是admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b
而我们错误的得到admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x3b\x2c
这样解密程序往往会抛出异常(Padding Error)
应用在web里的时候,往往是302或是500报错
而正常解密的时候是200
所以这时,我们可以根据服务器的反应来判断我们输入的iv

如果发送的rememberMe可以正确解析

否则会抛出异常,返回deleteMe

通过这一点的不同,我们可以向服务发出一个oracle:“我这个iv解密出的padding对不对?”

如果是对的,正确解析,如果是错的返回deleteMe,基于此反复发出Oracle来爆破iv,再控制iv来控制解密后的明文(也就是不需要key了)

这里还有一点,为什么需要一个合法用户的rememberMe,因为Shiro会获取用户信息,如果不是合法用户也会返回异常从而抛出deleteMe,这样Oracle就没办法实现了。

### 关于Apache Shiro 550反序列化漏洞详情 对于Apache Shiro存在的反序列化漏洞,具体到CVE编号为CVE-2016-4437而非CVE-2022-42889。此漏洞存在于Apache Shiro<=1.2.4版本中,在返回包的Set-Cookie中存在`rememberMe=deleteMe`字段可以作为识别该漏洞的一个标志[^1]。 当应用程序使用了不安全的方式处理rememberMe功能时,攻击者可以通过构造恶意输入来触发Java对象的反序列化过程,从而执行任意代码。这一特性使得攻击者能够在服务器端运行未经许可的操作,造成严重的安全隐患[^3]。 为了检测是否存在上述提到的安全风险,可借助专门开发出来的工具如ShiroExploit来进行测试;这些工具能够帮助确认目标环境是否容易受到此类攻击的影响,并提供相应的证据支持[^4]。 ### 修复措施建议 针对已知的Apache Shiro反序列化漏洞(CVE-2016-4437),官方推荐升级至更高版本以获得最新的安全性改进和支持。除了及时更新软件外,还应采取其他预防性策略: - **禁用Remember Me功能**:如果业务逻辑允许的话,考虑完全关闭记住我的选项。 - **加强依赖库管理**:定期审查项目所使用的第三方组件及其版本号,确保它们都是最新且经过充分审计的状态。 - **实施严格的输入验证机制**:无论是来自客户端还是内部服务之间的通信数据都应该被仔细过滤和校验,防止潜在危险载荷进入系统核心区域。 ```bash # 更新Shiro到最新稳定版 mvn install:install-file -Dfile=/path/to/apache-shiro-core-latest.jar \ -DgroupId=org.apache.shiro \ -Dversion=<new_version> \ -Dpackaging=jar ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值