什么是 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 原生序列化 |