1. 凌晨两点半的告警:Shiro应急的真实开场
做应急响应这行,半夜被叫起来是家常便饭。但不同告警的含金量差别很大,有些是误报,有些是扫描器在刷屏,真正让你从被窝里清醒过来的,是那种直接指向"反序列化"字样的拦截日志。
我印象比较深的一次,是某客户核心业务系统在晚上十一点半开始,WAF连续拦截了十几条命中规则"Apache Shiro反序列化攻击"的请求。告警里清楚写着源IP来自境外,目标路径是 /login,Cookie里带了一串长度超过6000字符的rememberMe参数。看到这个组合,基本可以断定不是误报——攻击者正在对Shiro框架的rememberMe功能做反序列化利用,也就是CVE-2016-4437。
Apache Shiro 1.2.4及更早版本使用了一个硬编码的默认AES密钥,导致攻击者可以自行构造合法的rememberMe Cookie,在服务端触发反序列化,最终执行任意命令。这个洞公开到现在已经很多年,但它在真实业务里出现的频率依然高得吓人。原因很现实:大量老系统从未升级,默认密钥从来没人改过,攻击者拿公开的密钥模板直接套,一打一个准。
这篇文章不是漏洞情报复述,而是把我在处置这类告警时真实的排查过程、判断依据和处置动作整理出来。从告警怎么确认、Payload怎么安全提取分析、日志里哪些特征值得追,到处置时先做什么后做什么,完整走一遍。适合刚接触应急响应,或者正在给自己业务系统做Shiro加固的同行参考。
1.1 告警进来之后,先别急着封IP
接到这类告警,没有现场经验的人第一反应往往是"封IP、断网、重启"。我不建议这么干,原因有两个:第一,攻击者的IP很可能已经换了几跳代理,封一个没意义;第二,攻击流量恰恰是判断漏洞是否存在、影响有多大的关键证据,你一断网,现场就毁了。
正确的第一步是"快确认、慢处置"。先用一两分钟做三件事:
- 确认目标资产:这个IP对应哪个业务系统,负责人是谁,是不是Java技术栈,有没有用Shiro。
- 确认告警真实性:调出WAF或IDS的原始日志,看是不是真的存在超长rememberMe Cookie,看payload形态是否符合反序列化利用链特征。
- 确认攻击时间线:从几点开始扫,扫了多少次,命中哪些路径,是否已有成功迹象。
这三件事做完,对整个事件的基本判断就有了。我习惯在脑子里过一遍"这事最坏能坏到什么程度"——答案是如果攻击者成功执行了命令,系统里极有可能已经留下了webshell、定时任务或对外连接。所以接下来的动作分两条线:一条是阻断,一条是取证。
1.2 五分钟判断"是不是Shiro,用没用默认密钥"
判断一个站点是不是Shiro,有个非常经典的探测方法:给请求随便带一个非法的rememberMe=xxx Cookie值,如果服务端返回的响应头里出现Set-Cookie: rememberMe=deleteMe,基本可以确定是Shiro的rememberMe机制在工作。这个指纹我用了无数次,在应急现场比什么扫描器都准。
版本判断要难一些,纯黑盒情况下Shiro不会在HTTP头里暴露版本号。但在应急响应场景里,手里通常有主机权限,直接看应用部署目录就行:解包后的WEB-INF/lib里找shiro-core-x.y.z.jar,版本号一眼就能看到。没有主机权限的话,就靠行为判断——响应头有deleteMe、Cookie加密结构和长度符合特征,先按漏洞存在处理。
比较头疼的是"是否用了默认密钥"。很多老Shiro应用,配置文件里根本没有显式配置securityManager.rememberMeManager.cipherKey,用的就是代码里写死的硬编码默认值。这个值早就跟着源码和文档公开了,攻击者知道,检测工具也知道。所以应急的时候,凡是没有证据表明密钥被改过,一律按默认密钥来评估——宁可虚惊一场,也别漏判。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CVE-2016-4437原理复盘:rememberMe从会话保持入口变成攻击入口
讲到这,有必要把漏洞原理讲透。不是为了背CVE描述,而是只有理解了rememberMe这条信任链是怎么设计的,才能明白为什么这个洞这么"痛",检测时该看哪里,处置时该改哪里。
2.1 rememberMe原本是个很贴心的功能
Shiro是Java领域最常见的权限框架之一,提供认证、授权、会话管理和加密能力。rememberMe就是"下次自动登录":用户登录成功后,Shiro把用户身份信息序列化成Java对象,用AES密钥加密,再做Base64编码,写进Cookie里。之后每次请求,服务器收到Cookie,先Base64解码,再用同一个AES密钥解密,最后反序列化还原用户身份,整个会话就恢复过来了。
你可以把它理解成一张酒店房卡:房卡里写了房间号,酒店前台用一个固定主密钥加密信息,客人每次进门,刷卡机解开卡片内容,核对房间号放行。设计上没有任何问题,前提是主密钥只有酒店知道。
2.2 攻击闭环:密钥公开加反序列化等于远程命令执行
CVE-2016-4437的问题就出在主密钥上。Shiro 1.2.4及之前版本,默认AES密钥是写死在代码里的,这个值早就跟着源码和文档公开了。对,就是那串kPH+bIxk5D2deZiIxcaaaA==,知道的人比很多内部系统的口令还多。
攻击链路非常干净:
- 攻击者用公开默认密钥,自己构造一个恶意Java对象的序列化字节。
- 用这个密钥做AES加密,再Base64编码,得到一个看起来完全合法的
rememberMeCookie。 - 把这个Cookie发到目标站点的任意接口。
- 服务器照常解码、解密,然后对这个"身份对象"做反序列化。
- 反序列化过程中,利用链(比如CommonsCollections系的gadgets)被触发,执行攻击者预置的命令,比如反弹shell、写webshell、下载木马。
问题出在两个致命点叠加:一是密钥固定且公开,攻击者能合法伪造Cookie;二是Java反序列化机制会无条件信任反序列化数据里的类定义,只要应用classpath里有可用的gadget链,就可能变成任意代码执行。这两个隐患原本是独立的,但在rememberMe机制里被完整地串成了一条链。
2.3 为什么这个洞到现在还在"诈尸"
现在回头看这个2016年的漏洞,你可能会觉得不可思议:为什么还在出问题?我在应急现场看到的原因基本是三类:
- 系统太老没人敢动:一些Java政务类、金融类、制造业应用一跑就是七八年,升级Shiro意味着重新测一堆老逻辑,业务方宁可扛着风险也不肯动。
- 只知道升级不知道改密钥:有些人以为升到新版就万事大吉,但从1.2.4升到1.2.5不是终点。后来CVE-2019-12422又因为AES-CBC模式配合Padding Oracle把问题带回来,处置这类漏洞,升级和换密钥必须一起做。
- 资产漏管:很多老系统早就没有负责人了,挂在某个角落一直跑,等扫描器发现了才想起"哦,这还有个系统"。
所以做应急响应不能只盯着眼前一台机器,得借着事件把整个资产面翻一遍,这才是真正有意义的地方。
3. 应急排查链路:告警确认、样本提取与影响面评估
回到现场。告警确认了,Shiro确认了,接下来是真正的排查。我把它拆成三步,每一步都有明确的产出。
3.1 第一步:把攻击流量"钉"在时间线上
首先从告警平台和WAF把原始请求包提出来。别只看聚合后的告警摘要,原始报文里才有细节:完整的Cookie值、User-Agent、请求参数、源IP、请求顺序。把时间线拉出来,看看攻击从什么时候开始、持续多久、尝试了多少种payload。
这里有个容易被忽略的动作:把攻击源IP在整个内网范围的访问记录都查一遍。攻击者往往不会只打一台机器,他会扫同一网段所有疑似Java业务,这一步决定了影响范围是"一台机器"还是"一片网段"。
时间线确定后,顺手做个标记:攻击开始前和后,系统有没有新增的对外连接、新增文件、新增计划任务。这是给后面的取证定方向的。
3.2 第二步:安全地提取并分析Payload
很多人拿到恶意Cookie后的第一反应是"本地解密看看"。方向对的,但操作上有讲究:绝对不要在受害机器上直接解密和反序列化。这句话我单独说一遍,因为在现场见过有人把攻击者payload粘到在线反序列化工具里跑的,等于亲手把攻击者的代码在自己的分析环境里执行了一遍。
正确做法是:
- 把原始请求里的
rememberMeCookie值完整拷出来,长度可能有几千到几万字符,注意别截断。 - 在隔离的分析环境(虚拟机或Docker)里,先做Base64解码,看前几个字节。如果是Java序列化对象,解码后开头应该是
AC ED 00 05。 - 确认是
AC ED 00 05后,基本能判断这是序列化攻击尝试。再进一步看字节里有没有特征字符串,比如CommonsCollections、TemplatesImpl这类gadget类名,能大致判断攻击者用的是哪条利用链。 - 如果payload还带了命令字符,比如
curl、wget、/bin/sh,就能直接还原攻击者想执行的命令。
解码后是乱码很正常,别拿文本工具直接看,要用xxd或hexdump看字节。下面这种命令就够用:
bash复制echo '粘贴Cookie值' | base64 -d > /tmp/payload.bin
xxd /tmp/payload.bin | head -20
strings /tmp/payload.bin | grep -iE 'commons|spring|tomcat|shell|curl|wget'
如果是加密后的payload,直接base64解码看到的是密文而非AC ED开头,这时需要先用默认密钥解密再检查。离线解密脚本有很多现成的,关键是务必跑在隔离环境里。
3.3 第三步:主机侧的横向排查
Payload分析完,至少能判断攻击者的意图。接下来要回答一个更现实的问题:攻击成功了吗?如果成功了,留下了什么?
这一步我按"进程-启动项-文件-网络-账号"五个方向查:
- 进程:有没有名字很随机的进程占着高CPU?
ps -ef翻一遍,特别留意Java进程的启动参数是否异常。 - 启动项:排查
/etc/cron.d/、/var/spool/cron/、/etc/rc.local、系统服务目录。反序列化RCE的常见落地方式是写一个反弹shell的定时任务,或者drop一个开机启动脚本。 - 文件:重点看
/tmp、/dev/shm、/var/tmp这些目录里按时间排序的最新文件。webshell常见藏身点是上传目录和静态资源目录,时间戳往往落在攻击窗口内。 - 网络:用
netstat -antpo看当前建立的对外连接,再结合流量日志看攻击窗口内的外联记录。反弹shell一定有一个非常规的外连目标地址。 - 账号:检查
/etc/passwd、/etc/shadow最近有没有新增账号,sudoers有没有被改。有些攻击者拿到权限后会给自己留后门账号。
这一套查完,影响面基本就清楚了。我见过最典型的结果是:payload打进来但利用链不匹配,攻击只停留在"尝试"阶段,主机干净;也见过攻击成功、crontab里躺着一行反弹shell。两种情况处置动作完全不同。
4. 日志分析取证要点:三层日志各自能告诉我们什么
应急响应本质上是"靠日志讲故事"。Shiro反序列化攻击会在三层留下痕迹:Web访问日志、应用框架日志、系统日志。每一层的信息密度不同,我分别说一下怎么看。
| 日志层 | 主要来源 | 最值得抓的特征 |
|---|---|---|
| 访问日志 | Nginx / Tomcat access log | 超长rememberMe、扫描行为、payload特征 |
| 应用日志 | catalina.out、Shiro/SLF4J输出 | Unable to load RememberMe、SerializationException、异常类名 |
| 系统日志 | secure/auth.log、cron、bash_history | 异常SSH登录、定时任务写入、可疑命令执行 |
4.1 访问日志:筛异常rememberMe流量
Nginx或Tomcat的访问日志是第一层,重点不是用肉眼看全部日志,而是用特征去筛:
- 按Cookie字段筛
rememberMe关键字; - 找Cookie值特别长的请求——正常登录的rememberMe Cookie一般就几百字节,恶意payload动辄几千甚至几万字符,长度本身就是很好的粗筛指标;
- 找同一源IP在短时间内对大量路径发请求的记录,扫描行为通常有明显的"撒网"特征。
两个典型命令:
bash复制# 统计带rememberMe的请求数量
grep -c 'rememberMe' access.log
# 筛出Cookie超长的请求并输出时间、来源IP、路径
grep 'rememberMe' access.log | awk 'length($0)>2000' | awk '{print $1, $4, $7}'
筛出来之后,把可疑请求的Cookie值拿去离线分析,跟排查链路里的方法对应起来。
4.2 应用日志:反序列化失败的报错里藏着关键信息
Shiro在解密、反序列化rememberMe Cookie失败时,会在日志里留下特征信息。比如Unable to load RememberMe cookie、SerializationException、ClassNotFoundException这类字样,后面往往跟着出问题的类名。对攻击者来说这是"没打进去"的标志,对我们来说则是"有人在打这个系统"的铁证。
如果攻击成功,还可能出现应用层的异常记录,比如Tomcat的catalina.out里出现异常的类加载记录、线程栈。日志时间范围建议取攻击窗口前后各顺延一小时,太久了日志量大,太短了容易漏。
有个实用经验:很多老系统的日志级别在INFO以下,一部分异常会被吞掉。如果日志里只看到请求记录但没有应用报错,不代表没被打,只能说明日志级别不够细。应急时如果条件允许,可以临时把日志级别调到DEBUG观察一段时间,但注意别在生产留太久。
4.3 系统日志:把时间线补完整
系统侧主要看四个:/var/log/messages或/var/log/syslog、/var/log/secure(或auth.log)、/var/log/cron、bash_history。
secure或auth.log里看SSH登录记录,确认攻击者有没有通过其他方式再次进入系统;cron日志看定时任务有没有被写入;bash_history看有没有执行过可疑命令,比如python -c反弹shell、下载远程脚本等。
把这些系统日志和访问日志里的时间点串起来,就能画出一条完整攻击时间线:几点开始扫描、几点投递payload、几点建立外连、几点写入计划任务。时间线是应急报告的灵魂,也是后续溯源其他攻击者的基础。
5. 处置与加固:止血、根除、防复发三件套
排查完了,下一步是处置。处置不是"删掉恶意文件就算完",要分止血、根除、防复发三层做。
5.1 止血:先隔离证据,再动手清理
先说结论:优先级最高的是保护现场,其次是阻断,最后才是清理。
- 隔离:把受害主机从业务网络上摘下来,但别直接关机或重启。内存里有攻击者的活动痕迹,一关机全没了。如果必须断开网络,先把进程状态和内存信息保留下来再断。
- 阻断:在WAF和防火墙上加规则,封禁攻击源IP,同时对异常rememberMe Cookie做统一拦截。这个动作要快,因为攻击者可能还在持续尝试。
- 备份证据:把原始日志、请求包、恶意文件、进程快照都做一份副本,存到离线位置。后面写报告、判断损失、溯源都要靠它。
5.2 根除:升级版本和换密钥一个都不能少
这是整个处置里最核心的部分。就CVE-2016-4437而言,仅仅删掉webshell、清掉crontab,等于只把地扫了没修漏水的管子——攻击者随时能再用同样的payload打进来。
标准动作是两项:
动作一:升级Shiro到已修复的版本。 官方在1.2.5版本就修复了默认密钥问题,但2016年的修复到了今天已经不够。实际处置中建议直接升到当前仍在维护的版本(1.x最新稳定版或2.x),同时注意兼容性,Spring Boot项目要重新验证Session管理、权限注解这些老逻辑。
动作二:显式配置一个新的随机密钥。 这是很多人升级完还会踩的坑:升级后如果不显式配置cipherKey,新版虽然不再使用公开默认值,但多实例部署时如果各实例密钥不一致,会带来会话兼容问题。生成密钥用下面这种方式:
bash复制# 生成128位随机AES密钥并Base64编码
openssl rand -base64 16
然后把得到的值配置到Shiro的shiro.ini或Spring配置里,再确认securityManager.rememberMeManager.cipherKey已经指向新值。
如果你对升级没把握,还有个兜底方案:业务上如果不需要"记住我"功能,直接把rememberMe关掉。Shiro里把rememberMeManager置为null,或前端不再下发rememberMe Cookie,等于直接废掉了攻击面,这比任何加固规则都彻底。
5.3 防复发:检测规则和资产基线
处置完成不代表事件结束。按我的习惯,后续两步必须做。
第一步,把这次攻击的特征沉淀成检测规则。具体包括:
- WAF规则:拦截超长
rememberMeCookie,比如超过1024字符直接告警; - 检测关键字:解密后出现
AC ED 00 05特征、CommonsCollections、TemplatesImpl等gadget类名时直接告警; - 响应头指纹:业务响应里带
rememberMe=deleteMe的,纳入资产指纹库,定期复查是否已升级。
第二步,重新核对资产台账。把内网所有Java应用拉一份清单,特别标注"是否用Shiro、版本多少、密钥是否改过、rememberMe是否开启"。我见过太多公司,事后才发现同网段还有好几个一模一样的Shiro老系统,这次只被打了一台,下一轮就会被扫到其他几台。
整理一个处置动作自查表,方便在现场逐项打勾:
| 动作 | 检查项 | 完成标准 |
|---|---|---|
| 确认 | 目标确实是Shiro且未改默认密钥 | 指纹和版本证据齐全 |
| 隔离 | 受害主机已脱离业务网络,证据已备份 | 进程、内存、日志副本离线保存 |
| 阻断 | WAF已拦截攻击源和异常Cookie | 攻击源无法再访问业务 |
| 分析 | payload已离线解密、利用链已识别 | 报告含攻击意图判断 |
| 根除 | Shiro已升级、密钥已更换 | 配置生效、业务验证通过 |
| 防复发 | 检测规则和资产台账已更新 | 全网同类型系统已排查 |
6. 复盘与经验沉淀:这个事件教会我的几件事
每次应急结束,我都会逼自己复盘一遍:哪些判断慢了,哪些动作多余,哪些信息本来可以提前拿到。这里挑三个最典型的盲区说说。
6.1 资产台账不全,应急就输了第一步
做网络安全,最大的痛点往往不是漏洞利用,而是你根本不知道内网里跑着多少台Shiro老系统。这次事件里,客户直到被攻击了才想起来某个老项目还挂在网上。所以每次应急,我都建议把运维的资产清单同步到安全侧,定期用指纹探针扫全网Java组件的版本。资产摸清了,漏洞应急至少快一倍。
6.2 日志留存时间太短,溯源窗口经常不够
很多系统日志只留30天,甚至只有7天。Shiro这类漏洞从扫描到getshell往往在几小时内完成,但攻击者可能在更早的时候就做过情报收集。如果日志留得短,溯源追溯不到最初那波侦察行为,报告说服力就大打折扣。日志集中管理这件事,前期投入不小,但真的值。
6.3 给同行的检查清单,随时可以拿出来用
最后,我把这次排障过程中验证过有效、踩过坑的经验浓缩成一份清单,方便下次遇到Shiro相关应急直接照着做:
- 第一,看到
rememberMe=deleteMe响应头就是Shiro,别浪费时间猜。 - 第二,Payload分析永远离线做,别拿生产机去解密,更别在分析环境里直接跑攻击者的东西。
- 第三,
AC ED 00 05是Java序列化对象的标志,先确认这个再谈利用链。 - 第四,升级和换密钥必须同时做,升级不换密钥等于白升一半。
- 第五,WAF拦截规则要定期根据新出现的利用链更新,老规则挡不住新链。
- 第六,处理完一台,必须把同网段同类系统全查一遍,这不算加分项,是必选项。
想练手的话,本地起一个带Shiro漏洞的应用,像Pikachu这类自带反序列化靶场的环境就够用,把"攻击payload进来、日志留痕、离线解码、主机排查、加固"的完整链路跑一遍,比看十篇理论文章都管用。日志分析类的靶场也行,比如玄机靶场第一章那种Linux日志分析的场景,把应急流程过一遍,能帮人把这些步骤内化成肌肉记忆。
