java 反序列化-1
基础
相关方法
# 序列化
ObjectOutputStream 类的 writeObject()
# 反序列化
ObjectInputStream 类的 readObject()
前提
实现 java.io.Serializable
接口才可被反序列化,而且所有属性必须是可序列化的(用 transient
关键字修饰的属性除外,不参与序列化过程)。
基本原理
Demo:
public static void main (String args[]) throws Exception {
String obj = "hello world!";
// 创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
// writeObject()方法将obj对象写入object文件
os.writeObject(obj);
os.close();
// 从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
// 恢复对象
String obj2 = (String)ois.readObject();
System.out.print(obj2);
ois.close();
}
- 序列化后写入 object 文件的数据如下
序列化后的数据开头包含两字节的魔术数字:ACED
。接下来是两字节的版本号 0005
的数据。此外还包含了类名、成员变量的类型和个数等。
- 写入 object 后,使用
readObject
方法读取序列化后的数据
漏洞就发生在当readObject()
方法被重写,反序列化该类时调用便是重写后的 readObject()
方法。如果该方法使用不当的话就有可能引发恶意代码的执行。
例如:
class MyObject implements Serializable{
public String name;
// 重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
// 执行默认的readObject()方法
in.defaultReadObject();
// 执行打开计算器程序命令
Runtime.getRuntime().exec(name);
}
}
当进行对象的反序列化时,就会执行readObject
方法里的命令执行方法:
实际情况下的反序列化会比较复杂,与 java 各类特性相关。
Java 反射特性
反射是大部分语言都有的,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的⽅法可以调用,总之通过反射,可以将Java这种静态语言附加上动态特性。
动态特性:
⼀段代码,改变其中的变量,将会导致这段代码产⽣功能性的变化
例如如下 Demo, 当不知道传入参数值时,是无法晓得他的实际作用。
Demo1:
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}
上述例子中包含了几个重要的与反射相关的方法:
- 获取类对象的方法:
forName
- 实例化类对象的方法:
newInstance
- 获取函数的方法:
getMethod
- 执行函数的方法:
invoke
获取类对象的方法
Java 中类本身也是一个对象,java.lang.Class
类的实例,这个实例称为类对象:
- 类对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法
想要获得一个类的属性和方法,就必须先获得该类的类对象。
方法:
obj.getClass()
如果上下文中存在某个类的实例 obj ,可以直接通过obj.getClass()
来获取它的类Test.class
如果已经加载了某个类,只是想获取到它的java.lang.Class
对象,那么就直接拿它的 class 属性即可。这个方法其实不属于反射Class.forName
如果知道某个类的名字,想获取到这个类,就可以使用 forName 来获取
利用类对象创建实例
假设目前有一个Person类:
public class Person implements Serializable {
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public Integer getAge() {
return this.age;
}
}
与直接new Person()
不同,基于反射必须先拿到类对象,然后通过类对象获取构造器对象,再通过构造器对象创建一个对象。
public static void main (String args[]) throws Exception {
Class PersonClass = Class.forName("Person");
Constructor constructor = PersonClass.getConstructor(String.class, Integer.class);
Person p = (Person)constructor.newInstance("cool", 18);
System.out.println(p.getName());
}
有了实例,就可以访问其属性,使用其方法。
利用反射执行代码
// java.lang.Runtime.getRuntime().exec("calc.exe");
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),
"calc.exe");
上述代码本质上就是调用了java.lang.Runtime.getRuntime().exec("calc.exe")
。
使用到了getMethod
和invoke
:
- getMethod
getMethod
的作用是通过反射获取一个类的某个特定的公有方法。Java中支持类的重载,不能仅通过函数名来确定一个函数。所以,在调用 getMethod 的时候,需要传给他需要获取的函数的参数类型列表。
而这里Runtime.exec
有6个重载:
这里使用第一个,所以使用getMethod("exec", String.class)
获取到了第一个方法。
- invoke
invoke 的作用是执行方法,它的第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是类对象
如果这个方法是一个静态方法,那么第一个参数是类
正常执行方法是 [1].method([2], [3], [4]…) ,在反射里就是 method.invoke([1], [2], [3], [4]…) 。
所以上述例子可分解为:
class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");
另一种常用的执行命令的方式是 ProcessBuilder
,通过反射来获取其构造函数,然后调用 start()
来执行命令:
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("open", "/System/Applications/Calculator.app")));
获取私有方法
当一个方法是私有方法(例如私有构造函数),能否通过反射执行它?
使用getDeclared
系列的反射,它与普通的 getMethod 、 getConstructor
区别:
getMethod
系列方法获取的是当前类中所有公共方法,包括从父类继承的方法getDeclaredMethod
系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了
getDeclaredMethod
的具体用法和 getMethod
类似, getDeclaredConstructor
的具体用法 getConstructor
类似。
例如,Runtime这个类的构造函数是私有的,之前需要用 Runtime.getRuntime()
来获取对象。其实现在也可以直接用 getDeclaredConstructor()
来获取这个私有的构造方法来实例化对象,进而执行命令:
class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true); // 获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");