什么是 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.2dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version scoperuntime/scope /dependency漏洞概述Shiro-550CVE-2016-4437 是 Apache Shiro 框架中的一个 高危反序列化远程代码执行漏洞 。攻击者可以通过构造恶意的 rememberMe cookie在目标服务器上执行任意 Java 代码。漏洞原理在 Shiro 1.2.4 及之前版本 中 AbstractRememberMeManager 使用了 硬编码的默认加密密钥private static final byte[] DEFAULT_CIPHER_KEY Base64.decode(kPHbIxk5D2deZiIxcaaaA);这个密钥是公开的攻击者可以1. 使用该密钥加密恶意序列化对象包含 Gadget 链如 CommonsCollections2. 将加密结果作为 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 cookie2. 用户下次访问 → 服务端读取 rememberMe cookie → 自动登录所以我们应该从 cookie 的读写 入手。搜索 rememberMe 关键字找到 CookieRememberMeManager.java 。加密流程详解用户登录时入口 rememberSerializedIdentity 方法这个方法接收的 serialized 参数已经是加密后的字节数组了。追溯谁调用了 rememberSerializedIdentity查看父类 AbstractRememberMeManager.java核心加密 convertPrincipalsToBytes 方法这里就是漏洞的关键位置 先序列化再加密。序列化 serialize 方法这里实际调用的是 DefaultSerializer.java#L45-L65这是标准的 Java 序列化任何实现了 Serializable 接口的对象都能被序列化。加密 encrypt 方法AbstractRememberMeManager.java#L523-L531AES 加密核心 AesCipherService.encrypt实际调用的是父类 JcaCipherService.java#L306-L317继续看 JcaCipherService.java#L319-L348最终加密 crypt 方法底层调用 cipher.doFinal(bytes) 完成 AES 加密。解密流程详解用户下次访问时入口 getRememberedSerializedIdentity 方法CookieRememberMeManager.java#L196-L249protected 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-L547AES 解密核心 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 原生序列化