yeoserial之URLDNS调用链分析

环境准备

package org.URLDns;

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


public class URLDNS {
    public static void main(String[] args) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\86183\\Desktop\\out.bin"));
        ois.readObject();
    }
}

利用链分析

通过上面一篇文章,搭建后,然后我们对最简单的URLDNS链进行分析

触发反序列化的⽅法是readObject,那么,我们可以直奔 HashMap 类的 readObject ⽅法:

 private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        reinitialize();
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                                             mappings);
        else if (mappings > 0) { // (if zero, use defaults)
            // Size the table using given load factor only if within
            // range of 0.25...4.0
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);

            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;

            // 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:键唯一,键值对存取无序, 由哈希表保证键唯一
在最后一行中,我们可以看到将HashMap的键名计算了hash,以此保证键唯一:

putVal(hash(key), key, value, false, false);

以至于我们为什么关注这个点?

因为ysoserial提到了hashCode计算操作时触发了DNS请求

我们可以看到这边调用链,以及代码,这边readObject调用了hash()
我们跟进去看

发现hash()方法又调用了hashcode()方法,由于key是java.net.URL类型

我们继续跟进hashCode()方法,因为是该类型,所以进入的hashcode()方法不一样(可能是跟我女朋友打电话,搞得我没法专心,断点下错了还是啥)

hashcode就是等于-1,所以进入下面一行,handler 是 URLStreamHandler 对象

之后我们继续跟进hashcode()方法

此处调用了 getHostAddress,我们再一次的f7跟进

这里的InetAddress.getByName,在给定主机名的情况下确定主机的IP地址,意思就是发起一次请求
如果还有不懂的,可以看看这篇文章 Java学习之网路编程

所以,⾄此,整个 URLDNS 的Gadget其实清晰⼜简单:

  1. HashMap->readObject()
  2. HashMap->hash()
  3. URL->hashCode()
  4. URLStreamHandler->hashCode()
  5. URLStreamHandler->getHostAddress()
  6. InetAddress->getByName()

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

要构造这个Gadget,只需要初始化⼀个 java.net.URL 对象,作为 key 放在 java.util.HashMap
中;然后,设置这个 URL 对象的 hashCode 为初始值 -1 ,这样反序列化时将会重新计算
其 hashCode ,才能触发到后⾯的DNS请求,否则不会调⽤ URL->hashCode()

参考文章

p牛的java漫谈
百度上的一些文章

posted @ 2021-01-22 15:12  0X7e  阅读(122)  评论(0编辑  收藏  举报