什么是 Shiro-550 漏洞?——从原理到实践的完整指南

📅 2026/7/6 6:56:38 👁️ 阅读次数 📝 编程学习
什么是 Shiro-550 漏洞?——从原理到实践的完整指南

前言

Shiro-550反序列化漏洞大约在2016年就被披露了,但感觉直到近一两年,在各种攻防演练中这个漏洞才真正走进了大家的视野,Shiro-550反序列化应该可以算是这一两年最好用的RCE漏洞之一,原因有很多:Shiro框架使用广泛,漏洞影响范围广;攻击payload经过AES加密,很多安全防护设备无法识别/拦截攻击...

最初碰到Shiro反序列化漏洞应该是在2017或者2018年的一个CTF线下赛。一直到现在,这个漏洞不断有新的知识出现,因此打算开个系列笔记重新记录漏洞学习过程。

环境搭建

下载源码https://github.com/apache/shiro.git
编辑 shiro/samples/web 目录下的 pom.xml, 将 jstl 的版本修改为 1.2

<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> <scope>runtime</scope> </dependency>

漏洞概述

Shiro-550(CVE-2016-4437) 是 Apache Shiro 框架中的一个 高危反序列化远程代码执行漏洞 。攻击者可以通过构造恶意的 rememberMe cookie,在目标服务器上执行任意 Java 代码。

漏洞原理

在 Shiro 1.2.4 及之前版本 中, AbstractRememberMeManager 使用了 硬编码的默认加密密钥

private static final byte[] DEFAULT_CIPHER_KEY = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

这个密钥是公开的!攻击者可以:

1. 使用该密钥加密恶意序列化对象(包含 Gadget 链,如 CommonsCollections)

2. 将加密结果作为 rememberMe cookie 发送给目标服务器

3. 服务器解密后反序列化恶意对象 → 执行任意代码

RememberMe 机制流程

在 CookieRememberMeManager 中,身份信息的处理流程如下:

序列化(加密)流程 :

用户登录(勾选记住我) ↓ PrincipalCollection(用户身份) ↓ serialize() → Java 序列化对象 ↓ encrypt() → AES 加密 ↓ Base64.encodeToString() → Base64 编码 ↓ 写入 rememberMe cookie

反序列化(解密)流程 :

读取 rememberMe cookie ↓ Base64.decode() → Base64 解码 ↓ decrypt() → AES 解密 ↓ deserialize() → Java 反序列化 ↓ 获取 PrincipalCollection → 自动登录

漏洞分析(代码层面)

代码分析思路:如何找到关键方法

从功能入口找线索

当我们要分析 Shiro 的 RememberMe 功能时,首先要问: 这个功能是怎么触发的?

1. 用户登录时勾选"记住我" → 服务端写入 rememberMe cookie

2. 用户下次访问 → 服务端读取 rememberMe cookie → 自动登录

所以我们应该从 cookie 的读写 入手。搜索 rememberMe 关键字找到 CookieRememberMeManager.java 。

加密流程详解(用户登录时)
入口: rememberSerializedIdentity 方法

这个方法接收的 serialized 参数已经是加密后的字节数组了。

追溯:谁调用了 rememberSerializedIdentity

查看父类 AbstractRememberMeManager.java

核心加密: convertPrincipalsToBytes 方法

这里就是漏洞的关键位置! 先序列化,再加密。

序列化: serialize 方法

这里实际调用的是 DefaultSerializer.java#L45-L65

这是标准的 Java 序列化,任何实现了 Serializable 接口的对象都能被序列化。

加密: encrypt 方法

AbstractRememberMeManager.java#L523-L531

AES 加密核心: AesCipherService.encrypt

实际调用的是父类 JcaCipherService.java#L306-L317

继续看 JcaCipherService.java#L319-L348

最终加密: crypt 方法

底层调用 cipher.doFinal(bytes) 完成 AES 加密。

解密流程详解(用户下次访问时)
入口: getRememberedSerializedIdentity 方法

CookieRememberMeManager.java#L196-L249

protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) { if (!WebUtils.isHttp(subjectContext)) { if (LOGGER.isDebugEnabled()) { String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " + "servlet request and response in order to retrieve the rememberMe cookie. Returning " + "immediately and ignoring rememberMe operation."; LOGGER.debug(msg); } return null; } WebSubjectContext wsc = (WebSubjectContext) subjectContext; if (isIdentityRemoved(wsc)) { return null; } HttpServletRequest request = WebUtils.getHttpRequest(wsc); HttpServletResponse response = WebUtils.getHttpResponse(wsc); String base64 = getCookie().readValue(request, response); // Browsers do not always remove cookies immediately (SHIRO-183) // ignore cookies that are scheduled for removal if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) { return null; } if (base64 != null) { base64 = ensurePadding(base64); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Acquired Base64 encoded identity [" + base64 + "]"); } byte[] decoded; try { decoded = Base64.decode(base64); } catch (RuntimeException rtEx) { /* * https://issues.apache.org/jira/browse/SHIRO-766: * If the base64 string cannot be decoded, just assume there is no valid cookie value. * */ getCookie().removeFrom(request, response); LOGGER.warn("Unable to decode existing base64 encoded entity: [" + base64 + "].", rtEx); return null; } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Base64 decoded byte array length: " + decoded.length + " bytes."); } return decoded; } else { //no cookie set - new site visitor? return null; } }
追溯:谁调用了 getRememberedSerializedIdentity

查看父类 AbstractRememberMeManager.java#L416-L432

核心解密: convertBytesToPrincipals 方法

这里就是漏洞利用的关键! 如果攻击者能控制解密后的数据,就能执行任意代码。

解密: decrypt 方法

AbstractRememberMeManager.java#L539-L547

AES 解密核心: JcaCipherService.decryptInternal

反序列化: deserialize 方法

AbstractRememberMeManager.java#L567-L569

实际调用的是 DefaultSerializer.java#L75-L92

危险就在这里! ois.readObject() 会执行对象的反序列化,如果数据是恶意构造的,就会触发 Gadget 链执行任意代码。

代码流程图总结

加密流程(登录时)

用户身份 (PrincipalCollection) ↓ AbstractRememberMeManager.rememberIdentity() ↓ convertPrincipalsToBytes() ↓ serialize() → DefaultSerializer.serialize() → ObjectOutputStream.writeObject() ↓ encrypt() → JcaCipherService.encrypt() ↓ AES/GCM/NoPadding 加密(带随机 IV) ↓ IV + 密文 拼接 ↓ Base64.encodeToString() ↓ CookieRememberMeManager.rememberSerializedIdentity() ↓ 写入 rememberMe cookie

解密流程(访问时)

读取 rememberMe cookie ↓ CookieRememberMeManager.getRememberedSerializedIdentity() ↓ Base64.decode() ↓ AbstractRememberMeManager.getRememberedPrincipals() ↓ convertBytesToPrincipals() ↓ decrypt() → JcaCipherService.decryptInternal() ↓ 提取 IV → AES/GCM/NoPadding 解密 ↓ deserialize() → DefaultSerializer.deserialize() → ObjectInputStream.readObject() ↓ 获取 PrincipalCollection → 自动登录

漏洞利用

这里的话我直接用工具就进行测试了

这里的话也可以伪造cookie去进行命令执行,测试时候记得删除cookie里面的jsessionid

利用流程
攻击者构造恶意序列化对象(含 Gadget 链) ↓ 使用默认密钥 AES 加密 ↓ Base64 编码 ↓ 发送 rememberMe cookie 给目标服务器 ↓ 服务器读取 cookie → Base64 解码 ↓ 使用默认密钥 AES 解密 ↓ Java 反序列化 → 触发 Gadget 链 ↓ 执行任意代码(如反弹 Shell)

防御建议

措施

优先级

说明

升级 Shiro 版本

🔴 高

使用 1.2.5+ 版本,默认使用随机密钥

自定义密钥

🔴 高

通过配置文件设置自己的 cipherKey

禁用 RememberMe

🟡 中

如果业务不需要,直接禁用此功能

审计依赖

🟡 中

移除不必要的包含 Gadget 的库

使用安全序列化

🟢 低

考虑使用白名单机制或替换 Java 原生序列化