Apache Shiro 550反序列化漏洞:原理剖析与加固实战

发布时间:2026/10/22 20:04:51
Apache Shiro 550反序列化漏洞:原理剖析与加固实战 前阵子做内网常规巡检某后台管理系统又报了 Apache Shiro 550 漏洞CVE-2016-4437。这个洞从公开到现在已经很多年了但每次看资产清单它依然活跃在一批老旧系统上。很多刚接触安全的朋友看到编号550就容易懵觉得这是什么神秘变种实际上它就是我们常说的 Shiro 反序列化 RCE根因一点都不复杂。这篇文章我不打算复述那些到处都能搜到的漏洞公告而是想从原理剖析讲起把硬编码密钥、AES 加密、Java 反序列化这条链路彻底拆开再给出一份在授权测试场景下可以直接落地的实战 SOP最后把排查经验和加固清单一起整理出来。无论你是开发、运维还是安全工程师这篇文章应该能帮你少踩几个坑。1. 漏洞全貌先搞清楚 Shiro550 是什么1.1 一句话版本记住这条触发链路Apache Shiro 是一个很常见的 Java 安全框架负责登录认证、权限控制、会话管理。它有一个非常方便的功能叫 RememberMe就是用户勾选记住我之后服务端会往浏览器写一个 rememberMe cookie下次访问时自动恢复登录态。Shiro550 的问题恰恰出在这个 cookie 上。触发链路可以压缩成这么一句话服务端使用一个写死在代码里的 AES 密钥对序列化后的登录对象进行加密并写入 cookie攻击者只要能拿到这个密钥就可以自己伪造一段精心构造的 Java 序列化数据同样用这个密钥加密成合法 cookie 发送给服务端服务端解密后直接进行反序列化攻击者的恶意对象便会在目标机器上被还原最终导致任意命令执行。所以这个漏洞的本质是两个问题叠加硬编码密钥泄露加密凭据反序列化入口缺少类过滤。单独看其中一个可能还不致命但组合在一起就成了一个稳定、易用、影响极大的远程代码执行漏洞。1.2 550这个称呼是怎么来的先说一个容易误解的点CVE-2016-4437 这个编号本身是官方分配的而Shiro550并不是官方叫法。业内之所以习惯管它叫 550是因为早期很多漏洞库在收录这个 Apache Shiro 漏洞时把它映射到了一个编号为 550 的漏洞条目里时间久了大家就这么叫开了。你如果去翻 Apache Shiro 自己的 CVE 列表会发现它后面还有个经常被放在一起比较的 CVE-2019-12422也就是很多人说的 Shiro721。两者都是反序列化问题但 Shiro721 的利用条件苛刻得多需要获得一个合法用户的 rememberMe cookie 才能继续攻击而 Shiro550 只需要知道默认密钥就能打门槛完全不在一个量级。日常沟通中你只需要记住团队小伙伴说扫到 Shiro550时他指的就是这个 CVE-2016-4437 反序列化漏洞对应 Shiro 1.x 系列小于 1.4.2 的版本。1.3 为什么到今天还在被反复利用按理说一个 2016 年公开的漏洞到现在应该已经没人用了才对。但我在实际巡检里发现Shiro550 的检出率一直居高不下原因不外乎三点。第一Shiro 在 Java 服务端框架里太常见了很多内部管理系统、后台预览系统都在用它做权限控制属于一个非常容易被攻击者盯上的入口。第二系统升级难度被低估了。Shiro 是嵌入在业务代码里的组件升级不是换个包这么简单涉及 Shiro 配置写法调整、过滤器链顺序验证、session 管理兼容性等一系列问题很多团队一拖就是好几年。第三也是最关键的许多人以为升级到新版本就安全了但根本没有处理硬编码密钥的问题。升级只是把默认密钥从新代码里删掉了如果业务代码里还显式配置着旧密钥漏洞依然是漏洞。这也是我写这篇文章的原因原理不搞清楚修复就只会照猫画虎。2. 原理剖析密钥硬编码与反序列化是怎么串起来的2.1 硬编码密钥藏在哪个环节Shiro 记住登录状态的核心类叫 AbstractRememberMeManager它内部有一个静态常量用于设置默认的加解密密钥。这个密钥是 Base64 编码后写死在源码里的解码出来刚好是 16 字节正好对应 AES-128 的密钥长度。关键点在于Shiro 的老版本代码里这个默认值会在创建 RememberMeManager 时被当成默认配置直接使用。如果开发者没有显式调用 setCipherKey 换成自己的密钥那么所有使用该版本 Shiro 的应用它们的 rememberMe cookie 加密密钥就是同一个。这就相当于全世界的门都用了同一把钥匙而且这把钥匙的图纸就贴在门框上。很多人问AES 不是够安全吗密钥不泄露的话攻击者根本解不开 cookie。但这里的问题就是密钥不是不泄露而是全局默认公开。攻击者不需要去破解密码学算法只需要把网上公开的默认密钥抄下来就能合法地加密自己的恶意数据。安全边界直接退化成了秘密是否公开而不是算法是否可破。2.2 反序列化从 cookie 到对象的过程再来看反序列化这一环。Shiro 在校验 rememberMe cookie 时会把 cookie 里的内容取出来先进行 Base64 解码再用 AES 密钥解密得到一段字节流。接着这段字节流会被交给 Java 原生的对象输入流去还原成对象大致逻辑是拿到字节流后用 readObject 直接读取。问题就出在这个直接读取上。Java 原生的反序列化机制在设计时就没有做类白名单校验只要目标 ClassLoader 能找到对应的类它就会按字节流里的描述把对象创建出来。所谓 gadget 链就是利用 Java 框架里一些本身就存在的危险类这些类在反序列化过程中会触发额外的方法调用经过一系列跳板最终能够执行任意代码。Shiro 这里没有额外的安全防护没有类似 ObjectInputFilter 的机制来限制可以反序列化的类于是攻击者只要能把精心构造的字节流喂给这个入口就能获得一个代码执行的机会。更有意思的是由于 rememberMe cookie 是攻击者自己构造并加密的整个输入内容完全可控这让漏洞的利用稳定性非常高。2.3 把链路串起来的完整请求示例为了让你直观感受整条链路我手绘一个正常请求和攻击请求的对比流程。正常用户登录成功后Shiro 会做四件事把登录主体对象PrincipalCollection序列化成字节流用默认密钥做 AES 加密再做 Base64 编码拼接到 rememberMe cookie 里返回浏览器。下次请求时服务端按相反顺序处理读取 cookieBase64 解码AES 解密得到原始序列化字节流再调用 readObject 还原对象恢复登录态。攻击请求则把第二步的登录主体对象换成了恶意 gadget 链的序列化数据其余处理流程完全一致。因为密钥是公开的攻击者可以离线完成整个加密和编码过程最终只需要发送一个构造好的 Cookie 头例如Cookie: rememberMeBase64编码后的加密payload服务端只要存在漏洞且能访问到 gadget 链依赖的类就会在反序列化时触发恶意逻辑完成命令执行或外带数据。这个链路里有一个很反直觉的地方攻击者甚至不需要知道目标系统里有什么类只需要在 payload 里引用目标环境中大概率存在的公共库即可因此漏洞的通用性极强。3. 实战 SOP授权场景下的检测与验证流程先说明一个前提下面的流程只适用于你拥有明确授权的测试目标比如自己的测试环境、公司分配的渗透测试任务、或者应急演练靶标。对没有授权的目标做任何验证都是违规行为这一点没有商量余地。3.1 第一步资产指纹识别Shiro 的指纹特征非常明显识别起来几乎不会费劲。最简单的方法是给目标发一个假的 rememberMe cookie内容随便填比如rememberMe1。如果目标使用了 Shiro 的 RememberMe 功能服务端在解析这个非法 cookie 失败后会在响应的 Set-Cookie 头里返回一个特征标记rememberMedeleteMe。实际测试时可以用 curl 快速验证curl -i -k -s -c rememberMe1 https://目标地址/ | grep -i rememberMe常见的响应特征有这么几类响应特征含义可信度Set-Cookie: rememberMedeleteMe基本可以确认是 Shiro高无 Set-Cookie 但有 rememberMe 字样可能是其他框架低登录页同时具备rememberMe参数辅助确认中需要注意如果目标部署了 WAF 或前置网关可能会把响应头里的 deleteMe 去掉但流量日志里往往仍有记录。单靠一个特征可能误判建议结合登录接口路径、Cookie 名称、返回页面特征一起判断。3.2 第二步密钥弱点的无害化验证指纹确认之后下一步是验证目标是否还在使用默认硬编码密钥。这一步的关键原则是优先做无害化验证不要一上来就执行系统命令。常见的无害化验证思路是使用 DNS 回连。用一个只触发 DNS 解析请求、不执行系统命令的反序列化验证链比如常见的 URLDNS 链把它作为 payload 序列化后再用候选密钥加密成 rememberMe cookie 发给目标。如果目标存在漏洞并且可以出网你会在自己可控的 DNS 服务上看到目标 IP 的回连记录。我可以给一个用于生成 cookie 的 Python 脚本框架它只负责 AES 加密和 Base64 编码payload 部分请使用你在授权测试中自行生成的验证数据import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import pad # 这里填入你准备测试的 Shiro 密钥注意是 Base64 字符串 KEY_B64 此处填写待测试的候选密钥 key base64.b64decode(KEY_B64) assert len(key) 16, 密钥长度必须是16字节 # payload 为一个反序列化工具的产物例如无害的 DNS 验证链 payload byour_serialized_java_object cipher AES.new(key, AES.MODE_CBC, AES.MODE_ECB) # 实际模式以目标为准 encrypted cipher.encrypt(pad(payload, AES.block_size)) cookie base64.b64encode(encrypted).decode() print(frememberMe{cookie})这里我要特别说明Shiro 不同小版本的 IV 取法存在差异而且网上流传的密钥字典有一大串手动一个个试的效率很低。实际工作中建议直接使用社区成熟的 Shiro 检测工具它们内部已经处理了版本差异和密钥字典。自己写脚本的价值在于理解原理真到实战还是用成熟工具更靠谱。一旦确认目标能解析用默认密钥加密的恶意对象基本上就可以判定漏洞存在。接下来要做的是记录证据而不是继续扩大战果。3.3 第三步确认后的影响判定与记录漏洞确认后很多人容易犯一个错误立刻在当前环境弹一个计算器或者直接执行命令去证明漏洞结果要么污染了业务数据要么留下了大量攻击痕迹甚至把生产环境搞崩。正确的做法是先判定影响面再做最小化确认。你需要记录三件事漏洞 URL 和服务器 IP精确到端口和路径使用的密钥特征和验证链类型证明可解密可反序列化当前的响应时间、响应头变化等关键证据截图。如果一定要做代码执行层面的确认建议选择无害命令比如执行id或hostname而且要放在授权的测试环境提前确认这台机器上没有敏感业务。任何写操作、反弹 shell 操作都需要额外的书面授权。不要觉得能执行命令和真的执行了命令是同一件事从安全测试合规的角度看两者差别很大。3.4 流量侧与日志侧的检测特征在应急响应和日常监控里我们不能只依赖主动扫描还要在流量侧建立被动检测能力。从流量角度Shiro550 的攻击请求有一个很明显的特征Cookie 里的 rememberMe 字段长度非常大。正常用户登录后生成的 rememberMe cookie 通常只有几百字节而反序列化攻击 payload 动辄几千字节甚至几万字节。你可以在 WAF 或网络层加一条规则单个 Cookie 中 rememberMe 的值超过 2048 字节就要告警。从日志角度反序列化失败时服务端会抛出类似com.esotericsoftware.kryo.KryoException、java.io.InvalidClassException、ClassNotFoundException这样的异常。建议对应用日志做关键字采集重点关注rememberMe、deserialize、ObjectInputStream相关的报错。很多攻击在真正成功之前会先有大量的探测尝试这些异常日志就是最早的告警信号。4. 常见问题排查与应急处理实录4.1 误报来源一框架默认页与自定义页的干扰我在巡检中遇到的误报相当一部分来自框架默认首页或静态资源页面。比如访问一个不存在路径时后端返回的 404 页面里恰好包含rememberMe字符或者某些前端框架自己定义了同名 cookie都可能让扫描器误判。排查时不要看到 deleteMe 就下结论至少要做两步验证先确认目标确实是一个 Java 应用再看 rememberMe cookie 是否由后端 Set-Cookie 主动写入而不是前端脚本写入的。必要时直接查看返回头里的完整 cookie 属性Shiro 写出的 cookie 通常带有 Path 和 Max-Age 参数。4.2 误报来源二密钥列表不全导致的漏报另一类常见问题是漏报。企业的二次开发框架往往会在父类里统一配置一个自定义密钥这个密钥可能也是硬编码的但不在网上公开的默认密钥字典里。扫描器如果只测默认密钥列表就会漏掉这一类目标。所以我的检测策略是分两步走第一步用公开字典做快速验证确认是否为常见默认密钥第二步如果第一步没有任何命中但指纹特征明显指向 Shiro就需要提取目标 jar 包或配置文件查一下代码里是否有setCipherKey的调用。对授权测试来说拿到源码后直接定位密钥是最可靠的方式。4.3 应急响应的处理顺序如果确认生产环境存在 Shiro550而且没有条件立即停机应急处理顺序建议如下优先在网络层封禁来源 IP尤其是扫描器和攻击源的 IP 段先把利用入口压住在 WAF 上临时拦截异常的 rememberMe cookie规则可以先粗暴一点比如 cookie 包含 base64 常见填充特征且长度可疑就拦截关闭服务器的出网访问阻断反序列化链的二次外带通道这一步对很多依赖 HTTP/DNS 回连的利用方式非常有效联系业务方评估是否能直接升级 Shiro 版本制定变更窗口。这里最怕的是只封 IP 不修代码。攻击者换个代理就回来了防护效果只是暂时性的。真正的闭环一定是修复代码或升级组件临时措施只是给你争取时间。5. 防御加固清单从应急到长效机制5.1 版本升级与密钥轮换修复 Shiro550 最核心的动作有两个升级版本和更换密钥两者缺一不可。版本方面需要升级到 1.4.2 或更高的稳定版本。新版本在默认配置里不再内置硬编码密钥同时增加了反序列化安全校验逻辑。但就像前面反复强调的只升级不换密钥等于白修旧的硬编码密钥如果继续显式配置在代码里攻击路径依然畅通。更换密钥的做法并不复杂。用任意安全随机数生成器产出一个 16 字节的随机值做 Base64 编码然后通过setCipherKey注入。需要注意密钥一旦更换所有已登录用户的 rememberMe cookie 都会失效用户需要重新登录这是业务上可以接受的代价。变更前最好和产品团队确认一下影响的用户范围选在低峰期操作。5.2 代码与配置层的改造建议如果你不是只负责一个系统而是维护一批 Java 应用建议把这些改造纳入统一的开发规范禁止在代码仓库里出现 Shiro 相关的硬编码密钥密钥统一放配置中心或环境变量并在密钥管理系统里做定期轮换如果业务根本不需要记住我功能直接在配置里禁用 RememberMe 管理器这是最简单粗暴的收敛方式对需要保留 RememberMe 的登录接口在过滤器链里单独控制确保只有登录接口能写入 rememberMe cookie定期扫描依赖库版本把 Shiro 纳入企业软件资产清单避免依赖传递引用了老版本而不自知。很多开发团队会习惯性地复制粘贴老代码配置把旧密钥也一起带过来。建议在 CI 阶段加一个密钥检测规则扫描代码中是否出现了已知的弱密钥或硬编码密钥特征值一旦命中直接让构建失败。5.3 监控与纵深防御最后说一下纵深防御。漏洞不会被彻底消灭我们能做的是在漏洞存在的窗口期尽量增加攻击成本。日志监控方面在日志平台配置针对反序列化异常的告警上面提到的ClassNotFoundException、InvalidClassException都是重点。流量监测方面重点关注 rememberMe cookie 的超常大小和短时间内的多次尝试。主机侧可以部署 RASP 产品在反序列化入口处做行为检测这是目前效果最直接的运行时防护手段。网络层面尽量收敛 Java 应用服务器的出网权限。很多反序列化利用链都需要目标服务器主动发起外连才能完成数据外带限制出网等于直接切断了命令执行后的通信通道。这个措施说起来简单但很多企业因为业务原因迟迟没做我建议把它当成一项长期安全基线来推进。对我来说Shiro550 最大的教育意义不是教我们怎么抓反序列化漏洞而是提醒我们安全框架的默认配置不等于安全配置。老版本留下了一把全世界通用的钥匙而新版本只是不再发这把钥匙过去发出的钥匙并没有自动作废。每次做修复复盘时我都会多问一句密钥换了吗配置中心里那个值是不是还是老代码里的 Base64 串如果你负责的系统还在用 Shiro现在就可以去查一下说不定会有意外发现。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询