返回
Featured image of post java 反序列化-1

java 反序列化-1

go!

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")

使用到了getMethodinvoke

  • 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");
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy