Java 反序列化分析从入门到放弃(一)
学习反序列化之前需要了解一些基础知识。
一、序列化和反序列化
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)。
当我们调⽤⼀个对象的⽅法时:
- 会优先去当前对象中寻找是否具有该⽅法,如果有则直接调⽤
- 如果没有,则去当前对象的⽗类中寻找,如果⽗类中有则直接调⽤⽗类中 的⽅法
- 如果没有,则去⽗类中的⽗类寻找,以此类推,直到找到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){
}
九、参考链接
- https://www.liaoxuefeng.com/wiki/1252599548343744/1260456790454816
- https://www.runoob.com/java/java-polymorphism.html
- https://blog.csdn.net/weixin_45538956/article/details/107987356
- https://xz.aliyun.com/t/6787
- https://blog.csdn.net/qq_36226453/article/details/82790375
- https://blog.csdn.net/weixin_42956945/article/details/81637843
- https://blog.csdn.net/bestone0213/article/details/47904107?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control
- https://blog.csdn.net/wbxaut/article/details/85262194
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可。