Java 反序列化分析从入门到放弃(一)

N 人看过

       学习反序列化之前需要了解一些基础知识。
在这里插入图片描述

一、序列化和反序列化

       Java 序列化是指把 Java 对象转换为字节序列的过程;Java 反序列化是指把字节序列恢复为 Java 对象的过程。对象序列化是对象持久化的一种实现方法,它是将对象的属性和方法转化为一种序列化的形式用于存储传输。反序列化就是根据这些保存的信息重建对象的过程。
可以参考:https://xz.aliyun.com/t/6787
为了方便理解,我对在同一个默认包下,存在的User类和Serialize_test_1类的代码进行了注释:

User 类:

import java.io.Serializable;


public class User implements Serializable {
		//定义类的私有属性name
        private String name;
        //定义setName方法
        public void setName(String name) {
        	//当前类User的name等于setName传递进来的name
        	this.name = name;
        }
        //定义getName方法,返回name值
        public String getName() {
        	return name;
        }

}

Serialize_test_1 类::

import java.io.*;

public class Serialize_test_1 {
	//序列化,返回值为字节类型,输入值为对象Object
	public static byte[] serialize(final Object obj) throws Exception {
		//创建字节数组输出流实例 btout,将所有发送到输出流的数据保存在该字节数组缓冲区中, 个人理解就是读数据并放入缓存区,读的数据以字节类型的数组保存在缓冲区
		ByteArrayOutputStream btout = new ByteArrayOutputStream();
		//创建objOut 对象输出流实例,也是类似以上的读数据,不过操作的是对象流,不是字节流,序列化对项的类,使其变得可以传输,操作
		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();
	}
	public static void main(String[] args) throws Exception {
		//实例化对象user,并设置其私有变量name值为posty
		User user = new User();
		user.setName("posty");
		//序列化对象
		byte[] serializeData = serialize(user);
		//文件流,创建文件user.bin并写序列化后的数据
		FileOutputStream fout = new FileOutputStream("user.bin");
		fout.write(serializeData);
		fout.close();
		// (User)表示把反序列化后的对象转为User对象,并赋值给已经声明为User类的变量user2
		User user2 = (User) unserialize(serializeData);
		//调用User的getName方法
		System.out.print(user2.getName());
	}
	
}

二、Java 方法重写

       在Java中,如果在⼦类中有和⽗类同名的⽅法,则通过⼦类实例去调⽤⽅法时,会调⽤ ⼦类的⽅法⽽不是⽗类的⽅法,这个特点称之为⽅法的重写(覆盖-Overide)。

当我们调⽤⼀个对象的⽅法时:

  1. 会优先去当前对象中寻找是否具有该⽅法,如果有则直接调⽤
  2. 如果没有,则去当前对象的⽗类中寻找,如果⽗类中有则直接调⽤⽗类中 的⽅法
  3. 如果没有,则去⽗类中的⽗类寻找,以此类推,直到找到object,如果依 然没有找到就报错了

所以对于反序列化中的readObject()方法来说,如果对readObject进行重写,并且重写后还能执行某些恶意方法,就可以进行反序列化攻击了。下面以Evil类和Serialize_test_Evil_demo类进行测试:

Evil 类:

import java.io.*;
public class Evil implements Serializable {
	public String cmd;
	//原测试为public不成功, 改为private
	//重写readObject()反序列化方法
	private  void readObject(java.io.ObjectInputStream stream) throws Exception{
		// TODO Auto-generated method stub
		//默认反序列化,反序列化非静态非瞬态字段
		stream.defaultReadObject();
		//调用exec方法执行命令cmd
		Runtime.getRuntime().exec(cmd);
	}
}

Serialize_test_Evil_demo 类:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Serialize_test_Evil_demo {
	//序列化,返回值为字节类型,输入值为对象Object
	public static byte[] serialize(final Object obj) throws Exception {
		//创建字节数组输出流实例 btout,将所有发送到输出流的数据保存在该字节数组缓冲区中, 个人理解就是读数据并放入缓存区,读的数据以字节类型的数组保存在缓冲区
		ByteArrayOutputStream btout = new ByteArrayOutputStream();
		//创建objOut 对象输出流实例,也是类似以上的读数据,不过操作的是对象流,不是字节流,序列化对项的类,使其变得可以传输,操作
		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();
	}
	public static void main(String[] args) throws Exception {
		//恶意cmd测试
		Evil evil = new Evil();
		evil.cmd = "calc.exe";
		//序列化对象evil并传递参数cmd="calc.exe"
		byte[] serializeData = serialize(evil);
		//通过自定义的readObject()反序列化
		//传递参数cmd,执行了恶意的readObject()方法
		unserialize(serializeData);
		System.out.print(evil.cmd);
}
}

三、Java 继承与向上转型

       当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。

  • 向上转型的对象调⽤的⽅法是⼦类覆盖或继承⽗类的⽅法,不是⽗类的⽅法
  • 向上转型的对象⽆法调⽤⼦类特有的⽅法

其实向上转型就是多态的一种实现方式,在反序列化中,如果在某一个类中找不到可以实现恶意方法,就可以去他的父类找,或者父类的父类。以extend_test 类为例:
extend_test 类:

import java.lang.*;
import java.lang.reflect.InvocationTargetException;
public class extend_test {
	    public static void main(String[] args) {               
	      Animal a = new Cat();  // 向上转型,子类类型向父类转 
	      //结果:吃鱼
	      a.eat();               // 调用的是 Cat 的 eat
	      //error:The method work is undefined for the type Animal
	      //向上转型只能是覆盖父类有的方法,work方法不在父类中
	      //a.work(); 
	      Cat c = (Cat)a;        // 向下转型,父类类型向子类转
	      //结果:抓老鼠
	      c.work();        		 // 调用的是 Cat 的 work
	  } 
}
	 
	abstract class Animal {  
	    abstract void eat();  
	}  
	  
	class Cat extends Animal {  
	    public void eat() {  
	        System.out.println("吃鱼");  
	    }  
	    public void work() {  
	        System.out.println("抓老鼠");  
	    }  
	}

四、Java 接口和回调

       Java的接口(interface)定义了纯抽象规范,一个类可以实现多个接口。

  • 接口也是数据类型,适用于向上转型和向下转型;
  • 接口的所有方法都是抽象方法,接口不能定义实例字段;
  • 接口可以定义default方法(JDK>=1.8)。

Interface_test 类为例:

//定义一个People接口
interface People {
		//接口的方法peopleList()
		 void peopleList();
		}

//Students继承接口
class Student implements People {
		 public void peopleList() {
		 System.out.println("I’m a student.");
		 }
		}
//Teacher继承接口
class Teacher implements People {
		 public void peopleList() {
		 System.out.println("I’m a teacher.");
		 }
		}
public class Interface_test {
		 public static void main(String args[]) {
		 People a; 		    //声明接口变量
		 a = new Student(); //实例化,接口变量中存放对象的引用
		 a.peopleList();    //接口回调,调用Student类的peopleList()方法
		 a = new Teacher(); //实例化,接口变量中存放对象的引用
		 a.peopleList();    //接口回调,调用Teacher类的peopleList()方法
		 }
		// 结果:
		// I’m a student.
		// I’m a teacher.
}

五、Java 反射机制

       Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
       Java 的反射机制把我们的代码意图都利⽤字符串的形式进⾏体现,使得原本应该是字符串的属性,变成了代码执⾏的逻辑,⽽这个机制也是后续的漏洞使⽤的前提。

以下是一个 Test 类和 Invoker_test 类的测试:
Test 类:

public class Test {
	//私有属性name
    private String name;
    //方法1
    public void setName(String name) {
    	this.name = name;
    }
    //方法2
    public String getmyName() {
    	String name = this.name + " I am OK!";
    	return name;
    }
}

Invoker_test 类:

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


public class Invoker_test {
		 public static void main(String args[]) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchFieldException  {
			 //方式一:通过getClass()获取类对象
			 //实例化一个Test 类 a
			 //Test a = new Test();
			 //Class testClass = a.getClass();
			 
			 //方式二:通过Class.forName()获取类对象,其中Test为未实例化的类名,
			 //类名需要填充绝对路径,包名.类名,同一个包下就不需要了
			 //Class testClass = Class.forName("Test");
			 
			 //方式三:通过-> 类名.class 方式,本例中即为Test.class
			 Class testClass = Test.class;
			 
			 //使用获取构造器方法生成构造器对象con
			 Constructor con  = testClass.getConstructor();
			 
			 //实例化Test类并命名为test
			 Test test = (Test) con.newInstance();
			 
			 //获取类testClass的方法setName,参数类型String.class,并赋值给set,即现在方法名为set
			 Method set = testClass.getDeclaredMethod("setName", String.class);
			 //获取类testClass的方法getmyName
			 Method getmyName = testClass.getDeclaredMethod("getmyName");
			 //获取类名
			 String className = set.getDeclaringClass().getName();
			 //获取方法名
			 String methodName1 = set.getName();
			 String methodName2 = getmyName.getName();
			 //通过反射调用方法set,实际就是setName,格式-> 方法名.invoke(对象名,方法传递的参数)
			 set.invoke(test,"are you ok?");
			 //获取方法getmyName执行的结果
			 String reasult = (String) getmyName.invoke(test);
			 //获取Test类的name的属性,赋值给field
			 Field field = testClass.getDeclaredField("name");
			 //设置私有属性也可以访问
			 field.setAccessible(true);
			 System.out.println("类名:------------> " + className);
			 System.out.println("属性名:  		-> " + field.getName());
			 System.out.println("属性类型:----------> "+field.getGenericType());
			 System.out.println("参数值:  		-> "+field.get(test).toString());
			 System.out.println("方法名1:----------> " + methodName1);
			 System.out.println("方法名2: 		-> " + methodName2);
			 System.out.println("方法名2返回的结果:---> " + reasult); 
		 }
}

执行结果:
在这里插入图片描述

六、Map的entrySet()使用

       由于Map中存放的元素均为键值对,故每一个键值对必然存在一个映射关系。Map中采用Entry内部类来表示一个映射项,映射项包含Key和Value。Map.Entry里面包含getKey()getValue()方法,Set<Entry<T,V>> entrySet(),该方法返回值就是这个map中各个键值对映射关系的集合。

While循环:

Iterator<Map.Entry<Integer, Integer>> it=map.entrySet().iterator();
	while(it.hasNext()) {
		Map.Entry<Integer,Integer> entry=it.next();
		int key=entry.getKey();
		int value=entry.getValue();
		System.out.println(key+" "+value);
	}

For循环:

for (Map.Entry<String, String> entry : map.entrySet()) {
   System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
  }

七、CommonsCollections

       Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,Commons Collections 被广泛应用于各种Java应用的开发。

下载地址:https://commons.apache.org/proper/commons-collections/

八、isInstance与instanceof

       isInstance是Class类的一个方法,以下例子表示a是否能强制转换为A。

if(A.Class.isInstance(a)){
};

instanceof 是一个操作符,a是不是A这种类型

if(a instanceof A){
}

九、参考链接

本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可。