Apache Shiro反序列化漏洞深度解析:从原理到实战代码审计
1. 项目概述:从一次真实的应急响应说起
去年,我参与了一次针对某中型互联网公司的应急响应。攻击者利用一个看似不起眼的登录接口,在几分钟内就拿到了服务器的最高权限。事后溯源,根因正是Apache Shiro框架的反序列化漏洞。这个案例让我深刻体会到,对于Java开发者或安全从业者而言,理解Shiro反序列化漏洞的原理、审计方法和修复手段,不再是纸上谈兵,而是一项关乎系统生死存亡的必备技能。Shiro作为一个强大且广泛使用的Java安全框架,其内置的RememberMe功能在带来便捷用户体验的同时,也因其默认的加密方式存在缺陷,成为了攻击者垂涎的“后门”。本文将从一个实战代码审计者的视角,彻底拆解Shiro反序列化漏洞的来龙去脉。我不会仅仅停留在漏洞复现的步骤上,而是会深入Java序列化机制、Shiro的源码实现、密钥的生成与爆破、以及不同利用链的构造原理,并分享在真实代码审计中如何快速定位和验证此类漏洞的实战经验。无论你是想深入理解漏洞原理的安全研究员,还是负责保障自身项目安全的开发工程师,这篇文章都将提供一条从理论到实践的清晰路径。
2. 漏洞核心原理深度剖析
要理解Shiro反序列化漏洞,必须穿透三层“迷雾”:Java原生反序列化机制、Shiro对RememberMe功能的处理流程,以及攻击载荷(Payload)的构造逻辑。这是一个环环相扣的过程。
2.1 基石:Java反序列化为何危险?
Java序列化是将对象的状态信息转换为可以存储或传输的形式(字节流)的过程,反序列化则是其逆过程。其危险性根植于两个关键设计:
readObject()方法的魔力:在反序列化过程中,Java虚拟机会自动调用被序列化对象的readObject()方法(如果存在)。这个方法的本意是让对象有机会在反序列化后执行一些自定义的初始化逻辑。然而,攻击者可以精心构造一个对象,在其readObject()方法中嵌入任意代码。当这个恶意对象被反序列化时,其中的代码就会被执行。- 利用链(Gadget Chain)的组装:单一的一个类通常很难直接造成严重的命令执行。攻击者需要找到一条从“起点”(如反序列化入口)到“终点”(如执行命令的
Runtime.exec())的调用链。这条链由多个类的方法组成,它们像多米诺骨牌一样,通过属性传递、方法调用,最终触发危险操作。常见的库如Commons-Collections、Commons-Beanutils中就包含了大量可以被串联起来的“齿轮”。
注意:并非所有反序列化操作都危险。危险的前提是:反序列化的数据源不可控,并且应用的ClassPath中存在可利用的链。Shiro的RememberMe功能恰好同时满足了这两个条件。
2.2 关键:Shiro的RememberMe功能如何工作?
Shiro的RememberMeAuthenticationFilter负责处理“记住我”功能。其核心流程如下:
- 登录成功:用户登录时勾选“记住我”,Shiro会生成一个包含用户身份等信息的
AuthenticationInfo对象。 - 序列化与加密:Shiro将这个对象进行Java序列化,得到一个字节数组。然后,它使用一个**密钥(AES对称加密密钥)**对这个序列化后的字节数组进行加密,并做Base64编码。最后,将编码后的字符串设置为Cookie(默认键为
rememberMe)返回给浏览器。 - 后续请求:用户再次访问时,浏览器会自动带上这个
rememberMeCookie。 - 解密与反序列化:
RememberMeAuthenticationFilter拦截请求,取出Cookie值,进行Base64解码,然后用相同的密钥进行AES解密。解密成功后,Shiro会毫无戒备地对解密出的字节数组直接调用Java的ObjectInputStream.readObject()方法进行反序列化,试图还原出最初的AuthenticationInfo对象以完成自动登录。
漏洞的致命点就在这里:如果攻击者能够伪造一个经过加密和编码的Cookie,并且服务端用它持有的密钥能够成功解密,那么后续的readObject()操作就会反序列化攻击者精心构造的恶意对象,从而触发利用链。
2.3 命门:默认密钥与密钥爆破
Shiro最广为人知的问题在于其默认密钥。在早期版本中,Shiro使用了一个硬编码在源码中的默认AES密钥:kPH+bIxk5D2deZiIxcaaaA==。如果开发者在配置Shiro时,没有在securityManager.rememberMeManager.cipherKey属性中显式地指定一个自定义的强密钥,那么系统就会使用这个默认密钥。
这意味着,攻击者只要知道目标使用的是存在漏洞的Shiro版本,就可以直接用这个默认密钥来加密自己的恶意序列化数据,构造出合法的rememberMeCookie。更糟糕的是,即使用户修改了密钥,如果密钥的强度不够(例如太短、常见单词),攻击者仍然可以通过爆破的方式来猜测密钥。由于加解密操作在服务端是必然发生的(每个带RememberMe Cookie的请求都会尝试),攻击者可以自动化地发送大量不同密钥加密的Payload,根据服务器的响应差异(如错误信息、响应时间)来判断密钥是否正确。
实操心得:在审计代码时,我第一眼就会去排查shiro.ini或SecurityManager的配置类。如果找不到显式的cipherKey配置,或者配置的密钥看起来是弱密钥(例如123456、companyname2023),那么这里就存在极高的风险。一个安全的密钥应该是足够长(如32字节)、完全随机生成的Base64字符串。
3. 漏洞利用链的演进与构造解析
Shiro反序列化漏洞的利用史,就是一场围绕ClassPath中可利用组件的“军备竞赛”。利用链的选择直接决定了漏洞的利用范围和杀伤力。
3.1 经典链:Commons-Collections
这是Shiro漏洞早期最常用的利用链,依赖commons-collections这个极其常见的组件。其核心是利用Transformer、ChainedTransformer、ConstantTransformer、InvokerTransformer等类,构造一个能在反序列化时自动调用Runtime.getRuntime().exec(cmd)的调用链。
构造过程简述:
- 构造一个
ChainedTransformer,它包含一系列Transformer。 - 其中关键一环是
InvokerTransformer,它通过反射可以调用任意对象的任意方法。我们在这里设置方法名为"getRuntime"(无参),然后下一个InvokerTransformer调用"exec"方法(参数为命令字符串)。 - 将这个
ChainedTransformer包裹到LazyMap或TransformedMap中,再放入一个可序列化的类(如AnnotationInvocationHandler或BadAttributeValueExpException)的成员变量里。
当这个精心构造的对象被反序列化时,其readObject()逻辑会触发整个转换链的执行,最终达到命令执行的目的。
3.2 扩展与替代链
随着commons-collections版本升级,一些类被修复,经典链可能失效。攻击者和研究者又发现了新的“战场”:
- Commons-Beanutils:利用
BeanComparator和PropertyUtils相关的链,不依赖commons-collections,在只有commons-beanutils的环境中同样有效。 - C3P0:这是一个数据库连接池库。其利用链通过反序列化触发JNDI注入,结合恶意的RMI或LDAP服务,实现远程类加载和代码执行。这在不出网(目标服务器不能访问外网)但能访问内部恶意JNDI服务的情况下有用。
- 无依赖链(Shiro原生链):这是最巧妙的一种。研究者发现,Shiro自身依赖的
commons-beanutils中包含了可用于构造链的类,因此即便应用没有显式引入其他第三方库,只要使用了存在漏洞的Shiro版本,就可能直接利用。这条链的通用性极强。
审计时的思考:在代码审计中,查看项目的pom.xml或lib目录,快速梳理依赖库的版本至关重要。发现commons-collections:3.1、commons-beanutils:1.9.2等特定版本,就要立刻联想到对应的利用链。同时,要明白利用链是“组合”出来的,攻击者会根据目标环境“因地制宜”地选择或组合不同的链。
3.3 利用工具与Payload生成
手动构造这些链极其复杂,因此我们通常使用工具。最著名的是ysoserial和针对Shiro的shiro_attack。
- ysoserial:一个生成各种Java反序列化Payload的通用工具。你可以指定利用链(如
CommonsCollections5)和命令,它会生成对应的序列化字节数组。 - 构造Shiro专属Cookie:得到序列化字节数组后,还需要用正确的AES密钥(默认密钥或爆破得到的密钥)进行加密,然后做Base64编码,最后格式化为
rememberMe=XXX的Cookie。shiro_attack这类工具将此过程自动化,集成了密钥爆破、多种利用链尝试、回显Payload生成等功能。
一个简单的本地测试思路:
- 使用
ysoserial生成Payload:java -jar ysoserial.jar CommonsCollections5 "calc.exe" > payload.ser - 编写一个简单的Java程序,读取
payload.ser文件,用Shiro的AesCipherService和已知密钥(如默认密钥)进行加密和Base64编码。 - 将输出的字符串作为
rememberMeCookie的值,用Burp Suite等工具发送给目标。
4. 代码审计实战:定位与验证漏洞
理论最终要服务于实战。在审计一个Java Web项目时,如何系统性地排查Shiro反序列化漏洞?
4.1 第一步:识别Shiro框架
- 配置文件扫描:全局搜索
shiro.ini、spring-shiro.xml、*ShiroConfig*.java等文件。查找SecurityManager、Realm、Filter等相关配置。 - 依赖审查:检查
pom.xml或gradle.build,寻找org.apache.shiro的依赖。确认其版本号。影响范围最大的版本通常是<= 1.2.4,但后续版本如果配置不当同样可能存在问题。 - 类与注解识别:在代码中搜索
@RequiresPermissions、@RequiresRoles等Shiro注解,或者Subject.getCurrentSubject()等API调用。
4.2 第二步:检查RememberMe配置
在找到的Shiro配置中,聚焦于RememberMe管理器(通常是CookieRememberMeManager)的配置。
危险配置示例(shiro.ini):
[main] # 没有设置cipherKey,使用默认密钥!高危! securityManager.rememberMeManager = org.apache.shiro.web.mgt.CookieRememberMeManager安全配置示例:
[main] # 显式设置一个强随机密钥 securityManager.rememberMeManager = org.apache.shiro.web.mgt.CookieRememberMeManager securityManager.rememberMeManager.cipherKey = wg4KYA5Wk8y6CjH2Tqk1fJYj5VvLpEoR7XzNcM0BmSdGtHrPq=审计要点:
- 是否存在cipherKey配置?没有就是高危。
- 密钥强度如何?如果密钥是
123456、companyname或kPH+bIxk5D2deZiIxcaaaA==(默认密钥),均为高风险。 - 是否完全禁用了RememberMe?对于后台管理系统等无需此功能的应用,最彻底的方式是移除相关Filter或管理器。
4.3 第三步:动态验证与漏洞利用
在代码审计中,光看配置还不够,需要动态验证漏洞是否存在。
- 使用探测Payload:利用
shiro_attack或手工构造一个使用默认密钥的简单Payload(例如一个不包含恶意链但结构正确的序列化对象),发送给网站的登录接口或任意接口,并附上伪造的rememberMeCookie。观察响应。 - 分析响应特征:
- 漏洞存在:服务器可能返回一个与不含Cookie时不同的错误页面(例如
500错误但堆栈信息中包含org.apache.shiro.io.ClassResolvingObjectInputStream、EncryptionException或反序列化相关错误),或者直接返回一个deleteMe的Cookie(这是Shiro在解密失败后尝试删除无效Cookie的行为,但某些版本下,解密成功但反序列化失败也会返回deleteMe,需结合其他特征判断)。 - 密钥正确:如果使用正确密钥(无论是默认密钥还是爆破所得),服务器通常不会返回
set-Cookie: rememberMe=deleteMe。如果使用错误密钥,服务器则会返回此Cookie。这是爆破密钥的核心判断依据。
- 漏洞存在:服务器可能返回一个与不含Cookie时不同的错误页面(例如
- 尝试利用:确认漏洞存在后,使用工具尝试不同利用链。如果命令执行成功,可能会在服务器上创建文件、执行命令等。在授权测试中,可以尝试执行
whoami、id、ping(注意DNSlog外带数据)等命令来验证。
常见问题与排查实录:
- 问题:发送Payload后,服务器总是返回
deleteMeCookie,即使使用了默认密钥。- 排查:首先确认Shiro版本。可能是版本较新,默认密钥已不是经典的
kPH+bIxk5D2deZiIxcaaaA==。尝试使用工具爆破密钥。也可能是Payload本身构造有问题,或者目标环境的ClassPath中没有对应的利用链,导致反序列化失败,Shiro依然会尝试删除Cookie。
- 排查:首先确认Shiro版本。可能是版本较新,默认密钥已不是经典的
- 问题:找到了密钥,但所有利用链都不成功。
- 排查:这非常常见。说明目标应用的依赖环境中没有可用的利用链。此时需要:
- 仔细分析项目的依赖树,寻找其他可能构成链的库(如
groovy、xstream、jackson等)。 - 考虑使用回显Payload。传统Payload是直接执行命令,但可能因为网络策略、无回显导致不知道是否成功。回显Payload是将命令执行的结果写入HTTP响应中,这样就能在页面上看到结果。一些高级工具已经集成了Tomcat、Spring、WebLogic等环境的回显Payload。
- 考虑结合其他漏洞,如文件上传,先上传一个包含恶意类的Jar包,再利用Shiro反序列化触发加载这个类。
- 仔细分析项目的依赖树,寻找其他可能构成链的库(如
- 排查:这非常常见。说明目标应用的依赖环境中没有可用的利用链。此时需要:
- 问题:在Spring Boot项目中,Shiro配置写在Java Config类里,如何审计?
- 排查:重点查看
@Configuration标注的配置类。寻找@Bean注解返回SecurityManager、RememberMeManager的方法。检查其中是否调用了setCipherKey()方法并传入了一个强密钥。
- 排查:重点查看
5. 修复方案与安全开发建议
发现漏洞后,修复必须彻底。以下方案按推荐程度排序:
5.1 根本解决:升级与安全配置
- 升级Shiro版本:始终使用Apache Shiro官方发布的最新稳定版本。新版本通常修复了已知的安全问题,并可能引入了更安全的默认行为。
- 强制设置强密钥:这是最重要的措施。必须在Shiro配置中,显式地设置一个足够复杂、随机生成的AES密钥。
- 生成强密钥:可以使用Shiro自带的工具类或在线生成一个Base64编码的32字节随机密钥。
import org.apache.shiro.crypto.AesCipherService; import java.util.Base64; AesCipherService cipherService = new AesCipherService(); byte[] key = cipherService.generateNewKey().getEncoded(); String base64Key = Base64.getEncoder().encodeToString(key); System.out.println("你的强密钥: " + base64Key);- 配置密钥:在配置文件中,确保该密钥被正确设置到
RememberMeManager。
- 考虑禁用RememberMe:如果应用场景不需要“记住我”功能,最安全的方式是在
shiroFilterFactoryBean的filterChainDefinitions中移除或禁用authc过滤器之外的RememberMe相关过滤器,或者直接不配置CookieRememberMeManager。
5.2 纵深防御:缓解与监控措施
- 反序列化过滤器:在应用层或WAF层部署反序列化过滤器,拦截并检查序列化数据流。例如,使用
SerialKiller、contrast-rO0等库,或自己实现ObjectInputStream的子类,重写resolveClass方法,严格限制允许反序列化的类白名单。这是最有效的运行时防护之一。 - 减少危险依赖:定期审查项目依赖,移除不必要的、已知存在反序列化利用链的库(如老版本的
commons-collections)。如果必须使用,请升级到已修复的版本。 - Java安全管理器:配置严格的Java安全策略(
java.security.policy),限制代码执行权限。但这通常比较复杂,对应用影响较大。 - 网络与主机层防护:限制服务器不必要的出网连接,可以防御依赖远程加载类(如JNDI注入)的利用链。使用RASP(运行时应用自保护)产品进行监控和拦截。
5.3 安全开发习惯
- 永不信任外部输入:将
rememberMeCookie值视为完全不可信的用户输入。Shiro的漏洞正是违背了这一原则,在解密后未经验证就直接反序列化。 - 密钥管理:像对待数据库密码一样对待加密密钥。不要将密钥硬编码在源码中,应使用安全的配置中心或环境变量来管理。在CI/CD流程中,自动检查配置文件是否包含默认密钥或弱密钥。
- 依赖安全管理:引入
OWASP Dependency-Check、Snyk等工具到CI流程中,自动扫描项目依赖的已知漏洞(包括Shiro及其相关库)。
在我经历过的多次审计和渗透测试中,Shiro反序列化漏洞的根源几乎都是“懒惰的默认配置”。修复它并不难,难的是建立起一套覆盖依赖管理、配置审查、运行时监控的完整安全开发生命周期。对于开发者来说,在shiro.ini里多写一行配置一个随机密钥,这个简单的动作,就足以挡住绝大部分自动化攻击脚本。而对于安全人员,理解这套机制,能让你在纷繁复杂的日志和流量中,迅速抓住那条名为rememberMe的“小尾巴”,从而守住防线。
