Shiro反序列化漏洞深度解析:从RememberMe机制到Java反序列化攻击链

1. 项目概述:从“黑盒”到“白盒”的漏洞认知之旅

在安全研究领域,Shiro反序列化漏洞是一个绕不开的经典课题。它不仅仅是一个CVE编号,更是一个理解Java安全、框架设计缺陷和攻击者思维的绝佳样本。很多初学者拿到一个漏洞编号,比如CVE-2016-4437,第一反应往往是去网上找一份现成的利用脚本(POC),运行一下,看到回显的“whoami”就宣告复现成功。但这真的够了吗?在我看来,这仅仅是“黑盒”测试的起点。真正的价值在于“白盒”分析——深入代码层面,弄清楚漏洞为何会产生、攻击链是如何构造的、框架的防御机制又为何失效。这个过程,才是从“脚本小子”迈向“安全研究员”的关键一步。

“Shiro反序列化漏洞代码分析”这个项目,正是要带你走完这段旅程。它面向的是已经对Web安全基础(如HTTP、Cookie、Session)和Java Web开发有初步了解,不满足于单纯运行POC,渴望理解底层原理的开发者或安全爱好者。通过这个项目,你将不再被动地接受“这里有个漏洞”的结论,而是能够主动地、像侦探一样,顺着代码的逻辑线索,亲手揭开漏洞的神秘面纱。你会发现,那些看似复杂的攻击载荷(Payload),其设计思路在源码面前变得异常清晰;你也会明白,为什么某些版本的Shiro可以被打,而另一些则免疫,这背后的判断依据都写在代码里。

2. 漏洞背景与核心原理深度拆解

2.1 Shiro框架的“记忆”机制:RememberMe与序列化

要理解这个漏洞,必须先理解Shiro框架中的一个便利功能:RememberMe(记住我)。这是一个提升用户体验的常见设计,用户登录一次后,关闭浏览器再打开,无需重新输入密码即可保持登录状态。Shiro是如何实现这个“记忆”功能的呢?答案就是序列化(Serialization)。

序列化,简单来说,就是把一个内存中的Java对象,转换成一串可以存储或传输的字节流的过程。反序列化则是其逆过程,将这串字节流还原成内存中的对象。当用户勾选“记住我”并成功登录后,Shiro会做以下几件事:

  1. 将用户的身份信息(如用户名)、登录时间等,封装成一个 PrincipalCollection 对象。
  2. 使用一个 固定的、硬编码在源码中的密钥(AES Key) ,对这个对象进行AES加密。
  3. 将加密后的字节流进行Base64编码。
  4. 将编码后的字符串,设置为名为 rememberMe 的Cookie,发送给用户的浏览器。

当用户再次访问网站时,浏览器会自动带上这个Cookie。Shiro的过滤器会拦截请求,发现 rememberMe Cookie后,便执行反向操作:Base64解码 -> AES解密 -> 反序列化,还原出 PrincipalCollection 对象,从而自动完成登录认证。

注意 :这里埋下了第一个祸根—— 硬编码的默认密钥 。如果开发者在部署应用时没有修改这个密钥,那么所有使用该版本Shiro的应用,其加密密钥都是公开的。攻击者可以轻易地伪造或解密Cookie。

2.2 漏洞的“命门”:Java反序列化攻击链

问题的核心在于第三步:反序列化。Java的反序列化机制有一个特性:在将字节流还原为对象时,会自动调用该对象的 readObject() 方法。如果被反序列化的类重写了 readObject() 方法,那么该方法中的代码就会被执行。

攻击者的思路由此产生:我能否构造一个特殊的字节流,当Shiro试图反序列化它时,最终触发一段执行任意命令的代码?答案是肯定的,这依赖于“反序列化利用链”(Gadget Chain)。这条链由一系列存在于项目依赖库(如commons-collections, commons-beanutils)中的类组成,它们像多米诺骨牌一样,通过巧妙的属性设置和方法调用,最终可以触发命令执行(如 Runtime.getRuntime().exec(“calc”) )。

Apache Commons Collections 3.2.1版本中著名的 TransformedMap InvokerTransformer 等类,就构成了这样一条经典的攻击链(通常称为CC链)。Shiro在反序列化时,并没有严格检查即将被反序列化的对象类型,它只是忠实地执行了 ObjectInputStream.readObject() 。如果攻击者能够提供一条精心构造的、包含恶意代码的CC链字节流,并且用正确的密钥(默认或泄露的)加密,那么当Shiro解密并反序列化这个Cookie时,恶意代码就会在服务器端执行。

2.3 漏洞利用的关键条件与演变

这个漏洞的利用需要同时满足几个条件:

  1. Shiro版本 :通常指<=1.2.4的版本,因其使用了默认的硬编码密钥。但后续版本若密钥泄露,同样可被利用。
  2. 存在可利用的依赖 :目标应用的ClassPath中必须包含存在反序列化利用链的库,如commons-collections <= 3.2.1。
  3. 密钥已知或可爆破 :攻击者需要知道AES加密的密钥,才能构造出能被正确解密的恶意Cookie。

随着漏洞的公开和修复,攻防也在升级:

  • 密钥爆破 :由于密钥长度固定(128位),一些工具开始尝试使用常见的密钥字典进行爆破,尝试解密Cookie来判断密钥是否正确。
  • 利用链的扩展 :除了CC链,安全研究人员陆续发现了多条新的利用链,如CB链(CommonsBeanutils)、无依赖的JRMP链等,扩大了攻击面。
  • Padding Oracle攻击(CVE-2020-1957) :这是一个与加解密模式相关的旁路攻击,允许攻击者在不知道密钥的情况下,通过服务端的错误响应差异,逐步解密或加密Cookie,危害极大。

3. 核心代码层析:从Cookie到命令执行

3.1 入口点: AbstractRememberMeManager#getRememberedPrincipals

让我们把视线聚焦到Shiro 1.2.4版本的源码。一切始于 org.apache.shiro.mgt.AbstractRememberMeManager 类。当请求到来时, RememberMeAuthenticationFilter 会调用其 getRememberedPrincipals 方法。

public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
    PrincipalCollection principals = null;
    try {
        byte[] bytes = getRememberedSerializedIdentity(subjectContext); // 1. 获取Cookie字节数组
        if (bytes != null && bytes.length > 0) {
            principals = convertBytesToPrincipals(bytes, subjectContext); // 2. 关键转换
        }
    } catch (Exception e) {
        // 日志记录...
    }
    return principals;
}

3.2 解密过程: DefaultRememberMeManager#decrypt

convertBytesToPrincipals 方法会调用 decrypt 来解密字节数组。在 DefaultRememberMeManager 中,我们可以看到AES解密的过程。

protected byte[] decrypt(byte[] encrypted) {
    byte[] serialized = encrypted;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
        serialized = byteSource.getBytes();
    }
    return serialized;
}

这里的 getDecryptionCipherKey() 返回的就是那个致命的密钥。在1.2.4版本中,它被硬编码为 kPH+bIxk5D2deZiIxcaaaA== (Base64编码后的AES-128密钥)。

3.3 漏洞触发点: AbstractRememberMeManager#deserialize

解密后得到的是序列化后的字节流,接下来就是最关键的 反序列化 操作。

protected PrincipalCollection deserialize(byte[] serializedIdentity) {
    try {
        ByteArrayInputStream bais = new ByteArrayInputStream(serializedIdentity);
        ObjectInputStream ois = new ClassResolvingObjectInputStream(bais); // 使用自定义的OIS
        PrincipalCollection principals = (PrincipalCollection) ois.readObject(); // 漏洞触发点
        ois.close();
        return principals;
    } catch (Exception e) {
        // 异常被捕获,但反序列化过程已执行!
    }
}

ois.readObject() 是Java原生反序列化的入口。 ClassResolvingObjectInputStream 是Shiro自定义的流,主要为了解析类名,但它并没有重写 resolveClass resolveProxyClass 方法来严格限制可反序列化的类(在后续修复版本中,这里得到了加强)。因此,当攻击者传入一个精心构造的、包含CC链的序列化对象字节流时, readObject() 会忠实地按照其内容还原对象,并执行链中 InvokerTransformer 等类的危险方法,最终达到命令执行的目的。

3.4 攻击载荷的构造逻辑

攻击者侧的POC构造,核心是以下几步:

  1. 组装利用链 :使用ysoserial等工具,指定利用链(如 CommonsCollections5 )和要执行的命令( touch /tmp/success ),生成一个恶意的序列化对象字节数组。
  2. 加密 :使用Shiro的默认密钥(或已知/爆破出的密钥),采用AES-CBC模式,对恶意字节数组进行加密。
  3. 编码与封装 :将加密后的字节数组进行Base64编码,然后设置为 rememberMe Cookie的值。

当这个Cookie被发送到存在漏洞的Shiro服务时,就会沿着上述代码路径,触发漏洞。

4. 漏洞修复方案与代码层面的防御演进

4.1 官方修复之路

Apache Shiro团队在漏洞曝光后,采取了多层次的修复策略,这些修复点都体现在后续版本的源码中:

  1. 强制修改默认密钥(1.2.5+) :最直接的修复。新版本在启动时,如果检测到使用的是默认密钥,会直接抛出异常,强制开发者必须在配置中显式地设置一个唯一的、高强度的密钥。

    // 在初始化CipherKey时进行检查
    if (DEFAULT_CIPHER_KEY_BYTES.equals(cipherKey)) {
        String msg = "The default cipher key is being used for remember me encryption. " +
                     "This is a security risk. Please set a unique key.";
        throw new IllegalStateException(msg);
    }
    
  2. 引入反序列化类白名单(1.2.6+) :这是更根本的修复。Shiro创建了 DefaultSerializer 类,并在其中整合了 SerializationFilter 。或者通过重写 ClassResolvingObjectInputStream.resolveClass 方法,只允许反序列化少数安全的、预期的类(如 SimplePrincipalCollection ),而拒绝所有其他类。

    // 近似逻辑的示例
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String className = desc.getName();
        if (!className.startsWith(“org.apache.shiro.”) && !className.startsWith(“java.lang.”)) {
            throw new InvalidClassException(“Unauthorized deserialization attempt”, className);
        }
        return super.resolveClass(desc);
    }
    
  3. 修复Padding Oracle漏洞(1.5.0+) :针对CVE-2020-1957,将加密模式从CBC改为GCM等认证加密模式,确保密文被篡改后解密会直接失败,不再泄露信息。

4.2 开发者的安全实践

从代码分析中,我们可以总结出对开发者至关重要的安全启示:

  • 永远不要使用默认密钥 :这是红线。必须在 shiro.ini 或Spring配置中,使用随机生成的强密钥。
    # shiro.ini 示例
    securityManager.rememberMeManager.cipherKey = your_strong_base64_encoded_key_here
    
  • 及时升级依赖 :将Shiro升级到最新稳定版,确保包含所有安全修复。
  • 控制依赖库 :即使Shiro本身修复了,如果项目中引入了存在危险链的旧版commons-collections等库,仍然可能通过其他反序列化入口点被攻击。应使用Maven的 dependency:tree 命令检查并排除或升级危险依赖。
  • 最小化反序列化入口 :在业务代码中,避免直接使用 ObjectInputStream 处理来自外部的数据。如果必须使用,应严格实施白名单过滤。

5. 动手调试与分析:搭建动态分析环境

读代码不如调试代码。要真正吃透漏洞,建议你搭建一个动态分析环境。

5.1 环境准备

  1. 漏洞环境 :使用Vulhub或vulfocus快速搭建一个Shiro 1.2.4的靶场。这比你自己编译一个老版本Web应用要方便得多。
  2. IDE :推荐使用IntelliJ IDEA。将其远程调试功能配置到靶场运行的Tomcat上。具体步骤是:在Tomcat的启动脚本( catalina.sh )中加入调试参数 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 ,然后在IDEA中创建“Remote JVM Debug”配置,连接至 localhost:5005
  3. 源码关联 :在IDEA中导入对应版本的Shiro源码(可以从Apache官网下载源码包)。确保IDEA中的源码版本与靶场中使用的Jar包版本一致。

5.2 关键断点设置

在源码中打下以下几个关键断点,然后从浏览器发送一个带有恶意Cookie的请求:

  • AbstractRememberMeManager.getRememberedPrincipals() :入口。
  • DefaultRememberMeManager.decrypt() :观察解密前后字节数组的变化。
  • AbstractRememberMeManager.deserialize() 重中之重 。在这里步进(Step Into) ois.readObject()
  • commons-collections 库的 TransformedMap.checkSetValue() InvokerTransformer.transform() 处打上断点。当攻击链被触发时,程序会跳转到这些地方。

5.3 调试心法与观察要点

  • 观察调用栈(Call Stack) :当程序在 InvokerTransformer.transform() 处中断时,仔细观察调用栈。你会清晰地看到从 readObject() 开始,到 AnnotationInvocationHandler.invoke() ,再到 TransformedMap.setValue() ,最终到 InvokerTransformer.transform() 的完整链条。这是理解“链”式调用最直观的方式。
  • 查看变量值 :在 InvokerTransformer 中,查看 iMethodName iParamTypes iArgs 这几个成员变量的值。你会看到它们被设置为 ”getRuntime” null null ,然后在后续调用中被组合成 Runtime.getRuntime() ,最终 iMethodName 变成 ”exec” iArgs 变成命令字符串。这个过程完美展示了攻击链是如何通过反射动态调用危险方法的。
  • 尝试修改 :在调试过程中,你可以尝试修改内存中的变量,比如将命令从 ”calc” 改成 ”whoami” ,观察执行结果的变化,加深理解。

6. 从分析到挖掘:漏洞研究思维的建立

完成一次完整的代码分析后,你的目标不应止步于此。这套方法论可以迁移到几乎任何漏洞的研究中。

  1. 定位入口点 :对于任何漏洞,首先在源码中全局搜索敏感函数(如 readObject exec eval 反序列化 关键词等),找到数据从外部输入到危险函数被调用的完整路径。
  2. 理解数据流 :跟踪用户可控的数据(如HTTP参数、Cookie、Headers、文件内容)是如何在代码中流动、被处理、最终到达漏洞触发点的。这个过程可能需要你熟悉框架的过滤器、拦截器、控制器等机制。
  3. 构造利用条件 :分析为了达到漏洞触发点,需要满足哪些前置条件(如特定的参数值、特定的类路径、特定的配置状态)。这对应着漏洞的“利用限制”。
  4. 探索绕过方法 :针对修复方案(如黑名单、WAF规则),思考是否存在逻辑缺陷或过滤不严的情况,从而构造出绕过的Payload。例如,Shiro早期白名单修复后,就出现过因使用 Class.forName 加载类而导致的绕过。

通过对Shiro反序列化漏洞的深度代码分析,你收获的不仅仅是对一个特定漏洞的理解,更是一套适用于二进制或Web漏洞的静态/动态分析组合拳。下次当你听到“Fastjson反序列化”、“Log4j2 JNDI注入”时,你不会再感到陌生和畏惧,因为你知道,拿起代码,设置断点,沿着数据流追踪下去,一切谜底都将在你眼前展开。这才是安全研究最本质、也最迷人的乐趣所在。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值