Shiro550
0x 01 概述
Apache Shiro 是一个轻量级的 Java 安全框架,主要用于实现登录认证、权限控制、会话管理和简单加密等功能。它结构清晰、易于集成,通过 Subject、SecurityManager 和 Realm 等组件,实现从用户身份验证到权限校验的完整安全体系,常用于中小型 Java Web 项目的权限与安全管理。
0x 02 漏洞原理
Apache Shiro框架提供了记住密码的功能RememberMe,用户登录成功后会将用户的登录信息加密编码,然后存储在Cookie中。对于服务端,如果检测到用户的Cookie,首先会读取rememberMe的Cookie值,然后进行base64解码,然后进行AES解密再反序列化。
从攻击者的角度来看,如果攻击者成功爆破出用于加密RememberMe字段的AES密钥,相当于存在一个Java的反序列化入口。倘若服务端使用了存在Java 反序列漏洞依赖的组件,例如常见的commons-collections3、commons-collections4、commons-beanutils等等,那么攻击者利用Shiro提供的反序列入口构造出恶意的Gadget链,从而实现RCE。
漏洞版本:
Apache Shiro <= 1.2.4
0x 03 漏洞环境搭建
配置文件pom.xml依赖,这里使用
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>添加依赖工件

这里使用的代码是P神的测试环境代码
https://github.com/phith0n/JavaThings/blob/master/shirodemo/src/main/webapp/login.jsp
0x 04 漏洞调试
在官方给出的漏洞分析中,https://issues.apache.org/jira/browse/SHIRO-550
主要分析类org.apache.shiro.web.mgt.CookieRememberMeManager和org.apache.shiro.mgt.AbstractRememberMeManager中。
主要从两个方向进行分析源码,从CookieRememberMeManager类中是如何进行序列化加密的角度,从getRememberedSerializedIdentity则是从反序列化解密角度。
默认key位置
shiro-core-1.2.4-sources.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.java文件中DEFAULT_CIPHER_KEY_BYTES变量。
反序列加密
全局搜索CookieRememberMeManager类,发现调用rememberSerializedIdentity函数用于处理Cookie序列化逻辑,我们直接查看引用,看看哪里调用了这个函数就可以进一步跟进到加密逻辑。

查看引用后发现只有抽象函数AbstractRememberMeManager也就是CookieRememberMeManager继承的抽象类中的函数调用rememberIdentity了rememberSerializedIdentity函数

查看rememberIdentity函数逻辑,传入两个参数,分别是subject和accountPrincipals。 subject是Shiro框架中用于代表特定身份的一个类,在此方法中用于区别不同的用户登录,PrincipalCollection是Apache Shiro 框架中的一个接口,用于表示一个或多个身份主体(principal)的集合,这里的accountPrincipals存储了用户身份认证信息。

这里使用guest/guest登录,可以查看到相关认证信息。

继续跟进函数convertPrincipalsToBytes,主要逻辑是将存储认证信息的accountPrincipals变量转为字节形式,其中encrypt函数就是对accountPrincipals变量进行加密。

encrypt函数调用函数getCipherService实例化对象cipherService,直接debug可以看到使用的是AES CBC PKCS5Padding方法进行加密,getEncryptionCipherKey获取AES加密KEY,一路跟进调用,可以发现是硬编码DEFAULT_CIPHER_KEY_BYTES常量值为kPH+bIxk5D2deZiIxcaaaA==Base64解码。




到此加密逻辑部分就差不多了,为了完整性,补充一下
如果使用RememberMe功能成功登录的话,调用函数rememberMeSuccessfulLogin,进一步调用onSuccessfulLogin方法

onSuccessfulLogin方法判断是否启用RememberMe功能,进一步调用函数rememberIdentity

整个调用逻辑:
rememberMeSuccessfulLogin --> onSuccessfulLogin --> rememberIdentity --> convertPrincipalsToBytes --> encrypt
反序列解密
在DefaultSecurityManager类中getRememberedIdentity函数接受用于区分身份认证的参数subjectContext,紧接着调用AbstractRememberMeManager抽象类下的getRememberedPrincipals函数。

从getRememberedPrincipals函数中,我们可以大概看出主要逻辑在函数getRememberedSerializedIdentity和convertBytesToPrincipals当中

getRememberedSerializedIdentity函数主要判断Cookie是否合法以及deleteMe字段等等,把subjectContext当中的Cookie字段取出并Base64解密后返回

这里的ensurePadding函数也仅仅是对Base64判断长度补充=等,提高鲁棒性吧

convertBytesToPrincipals函数主要对getRememberedSerializedIdentity返回的Cookie字节形式进行解密,以及反序列化处理

decrypt函数我们可以看到几乎跟前面encrypt一样的代码,无非是一个加密一个解密,经典的AES对称加密算法

deserialize函数对解密后的字节进行反序列化处理,getSerializer函数用于获取序列化器,这个序列化器在AbstractRememberMeManager构造函数中有初始化,是一个为DefaultSerializer的序列化器


DefaultSerializer类的deserialize函数调用了ClassResolvingObjectInputStream类最后使用resolveClass进行反序列化,就不再赘述。
0x 05 反思
Shiro550在实战中遇到的次数还是不算少的,第一次打攻防就是用Shiro打进内网的,给我的印象还是十分深刻的。
Shiro550总的来说提供了一个Java 的反序列化入口点,让那些看起来没那么有用的Java反序列化Gadget链在实战中有机会大放异彩,不然像PHP的反序列化构造链在实战中能利用的机会少之又少。在目标服务器缺少必要反序列化依赖的情况下,也就是常说的Shiro 有key无链情况下,如果能通过heapdump或者其他一些渠道获得服务器依赖版本的情况下,也可尝试手动构造一些新链,毕竟能一把梭的工具基本都具有时效性。