0
点赞
收藏
分享

微信扫一扫

Java反序列化漏洞——反射与反序列化基础

        ​​​Java安全中除了常见的OWASP TOP 10相关的漏洞,还有大部分的漏洞是与JAVA反序列化相关的,而JAVA反序列化又与JAVA的反射机制相关,因此有必要对JAVA反射进行了解学习。JAVA反射即JAVA代码在JAVA JVM虚拟机中运行时,对于任何一个类可以动态的获取到它的class对象、属性、方法相关的信息,对于任何一个对象,可以获取到它的任意一个方法和属性,而不管这些方法、属性拥有什么样的权限修饰符。

通常有以下几个步骤去实现JAVA反射:

1、 获取到类的class对象

2、 通过反射创建类对象

3、 通过反射获取类属性、方法、构造器

一、获取到类的class对象

        在Java中存在一个特殊的类java.lang.Class,通过这个类可以创建任何一个指定的类的对象,也就是class对象,并且这个过程由JVM虚拟机自动完成。在反射中,要获取一个类或者调用一个类的方法,我们首先需要获取到该类的class对象。获取class类对象有四种方法:

(1)、使用Class.forName方法

这种方法需要知道类的全路径名称,比如:

Class clazz = Class.forName("java.lang.String");

System.out.println(clazz);

forName中有一个函数forName0,其中一个参数initialize默认为true,如下:

Java反序列化漏洞——反射与反序列化基础_反射

此时会自动初始化该Class对象。观察如下的代码:

package com.serialize.test;
public class TrainPrint {
    {
        System.
out.printf("Empty block initial %s\n",this.getClass());
    }
    static {
        System.
out.printf("Static inital %s \n",TrainPrint.class);
    }
    public TrainPrint(){
        System.
out.printf("Inital %s \n",this.getClass());
    }
}

运行一下:

Java反序列化漏洞——反射与反序列化基础_Java序列化_02

可以看到首先执行的是static静态代码块,然后执行的是空代码块,最后执行的是构造函数。Static{} 就是在“类初始化”的时候调用的,而{}中的代码会放在构造函数的super()后面,但在当前构造函数内容的前面。而forName中的initialize=true其实就是告诉java虚拟机是否执行”类初始化”,假设我们有如下函数,其中name参数可控:

package com.serialize.test;
public class reflectTest {
public void ref(String name) throws Exception{
Class.forName(name);
}
}

构造恶意类:

package com.serialize.test;
import java.io.IOException;
public class calcRun {
static {
Runtime rt = Runtime.getRuntime();
String[] commands = {"cmd","/c calc"};
try {
Process pc = rt.exec(commands);
} catch (IOException e) {
e.printStackTrace();
}
}

}

运行结果:Java反序列化漏洞——反射与反序列化基础_反射_03

跟踪一下代码,可以看到initialize为true:

Java反序列化漏洞——反射与反序列化基础_java_04

(2)、使用ClassLoader.getSystemClassLoader().loadClass()方法,比如:

Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime");

Constructor con= clazz.getDeclaredConstructor();

con.setAccessible(true);

clazz.getMethod("exec",String.class).invoke(con.newInstance(),"calc");

这种方法也需要知道类的全路径名称。

(3)、使用类名.class方法获取class对象,如下:

System.out.println("2、通过类名.class创建class对象:");

Class clz =Person.class;

System.out.println(clz);

(4)、使用对象.getClass()方法,首先创建一个对象,然后再调用对象的getClass()方法,如下:

System.out.println("1、通过对象.getClass()创建class对象:");

Person man = new Person();

Class clazz = man.getClass();

System.out.println(clazz);

二、通过反射创建类对象

1、通过Class对象的newInstance()方法

Class clz = Apple.class;

Apple apple = (Apple)clz.newInstance();

2、 通过Constructor获取构造函数对象,再通过构造函数对象的newInstance()方法去创建类对象。相关的方法包括:

(1)   getConstructor(Class…parameterTypes) 根据参数类型获取相应的构造函数,但是只能获取public类型的,不能获取父类的;

(2)   getConstructors()获取所有的构造函数,但是只能获取public类型的,不能获取父类的;

(3)   getDeclaredConstructor(Class…parameterTypes) 根据参数类型获取相应的构造函数,可以获取public、 protected和private类型的构造函数,不能获取父类的;

(4)   getDeclaredConstructors()获取所有的构造函数,可以获取public、 protected和private类型的构造函数,不能获取父类的;

示例:

Class clz = Apple.class;

Constructor constructor = clz.getConstructor();

Apple apple = (Apple)constructor.newInstance();

通过Constructor对象创建类对象可以选择特定构造方法,而通过Class对象只能使用默认的无参数公有构造方法。

三、通过反射获取类属性、方法、构造器

getMethod方法,相关的方法包括:

getMethod(String name,Class…parameterTypes) 根据方法名和参数的类型来获取应对的方法,不过只能获取public类型,不能获取父类的方法 getMethods() 获取类所有的方法

getDeclaredMethod(String name,Class…parameterTypes) 根据方法名和参数的类型获取对应的方法,可以获取public、protected和private类型的方法,不能获取父类的方法。

getDeclaredMethods() 获取所有的方法,可以获取public、protected和private类型的方法,不能获取父类的方法。

该方法的第一个参数name是要获得方法的名字,第二个参数parameterTypes是按声明顺序标识该方法形参类型,比如String.class,int.class等

invoke方法:

Public Object invoke(Object obj,Object…args)是Method类的方法,用于调用前一步所获取的方法。

       该方法的参数是一个Object类型,也就是​调用该方法的对象​,第二个参数是一个可变参数类型,也就是该方法的实参

示例:

//获取反射中的class类

Class clazz = Class.forName(“java.lang.Runtime”);

//获取反射类的方法

Method getRuntimeMethod =clazz.getMethod(“getRuntime”);

//调用方法获取runtime类

Object runtime =getRuntimeMethod.invoke(clazz)

如果一个类,其中只有一个构造方法,并且是私有的,怎么解决?例如Runtime类:Java反序列化漏洞——反射与反序列化基础_Java反序列化_05

下面是直接使用newInstance直接去创建类对象,提示失败:Java反序列化漏洞——反射与反序列化基础_Java反序列化_06

这时候要通过​类名.方法名​的方式去调用,比如Runtime.getRuntime()。使用如下的代码再次测试成功:Java反序列化漏洞——反射与反序列化基础_Java序列化_07

Object runtime =getRuntimeMethod.invoke(clazz);

与Object runtime = getRuntimeMethod.invoke(null);效果是一样的。

将以上的代码合并一下:

public class test {
public static void main(String[] args) throws Exception {
//合并后的POC
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(null),"calc");

getDeclared方法系列的反射示例:

public class test {
public static void main(String[] args) throws Exception {
//getDeclared方法
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec",String.class).invoke(m.newInstance(),"calc");

注意:这里使用了一个方法setAccessible,这个是必须的。我们在获取到一个私有方法后,必须用setAccessible修改它的作用域,否则仍然不能调用。

反射器的调用比较灵活,对于同一个类中的方法的调用几种不同的方式,具体如下:

首先创建一个类:

package com.serialize.test;
public class animalClass {
    private String kinds;
    private String name;
    public animalClass(String kinds,String name){
        this.kinds = kinds;
        this.name = name;
    }
    public void sayHello(){
        System.
out.println("this animal is " + this.kinds +" , it's name is "+this.name );
    }
}

下面通过不同的方式来调用这个类中的方法:

package com.serialize.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class refectionTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //第一种,传统的调用方法
        System.
out.println("第一种调用方法:");
        animalClass dog = new animalClass("dog","dahuang");
        dog.sayHello();
        //第二种,通过反射的方式,并且以构造函数生成类对象:
        Class clazz = Class.forName("com.serialize.test.animalClass");
        Constructor constructor =clazz.getDeclaredConstructor(String.class,String.class);
        animalClass dogClass =(animalClass)constructor.newInstance("dog","lily");
        Method sayHelloMethod = clazz.getMethod("sayHello");
        //2.1 通过反射获取函数名,然后通过invoke的方式进行调用
        System.
out.println("第二种调用方法:");
        sayHelloMethod.invoke(dogClass);
        //2.2 直接调用对象的方法名,这里的对象可以是构造函数的生成的类对象
        System.
out.println("第三种调用方法:");
        dogClass.sayHello();
    }
}

执行结果:

Java反序列化漏洞——反射与反序列化基础_Java反序列化_08

四、Java反序列化

        在JAVA的世界,万物皆对象,在JVM虚拟机中存在的都是对象,当对象需要进行保存或者在网络中传输时就需要对对象进行序列化处理,因此引出了序列化与反序列化的概念。序列化是将对象转换为字节序列,转换后可以进行存储或者在网络上进行传输,主要使用到的是ObjectOutputStream类中的writeObject()方法,该方法对参数指定的obj对象进行序列化,把字节序列写入到一个目标输出流中。反序列化即序列化的逆向操作,将字节序列转换为对象,主要使用以的是ObjectInputStream类的readObject()方法,该方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回,后续可以去调用该对象的方法。如果一个类要被实例化,那么这个类必须要实现Serializable接口或者Externalizable接口。

下面是示例:

创建一个Person对象:

package com.serialize.test;
import java.io.Serializable;
public class Person implements Serializable {
public String name;
private Integer age;
private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}

然后对Person对象进行序列化以及反序列化,测试代码如下::

package com.serialize.test;
import java.io.*;
public class serializerTest {
public static void main(String[] args) throws Exception {
SerializerPerson();
DeserialierPerson();
}
public static void SerializerPerson() throws Exception{
Person person = new Person();
person.setAge(18);
person.setName("lilei");
person.setSex("男");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:/person.txt"));
objectOutputStream.writeObject(person);
System.out.println("Person 对象序列化成功");
objectOutputStream.close();
}
public static void DeserialierPerson() throws IOException,ClassNotFoundException{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:/person.txt"));
Person person1 = (Person) objectInputStream.readObject();
System.out.println(person1);
System.out.println(person1.getName()+" " + person1.getAge()+" " + person1.getSex());
objectInputStream.close();
}

}

执行完成后可以看到E盘生成的文件,使用notepad++打开:Java反序列化漏洞——反射与反序列化基础_java_09

可以看到序列化后的标志:aced 0005

五、总结

        这是学习Java安全的开始,很多代码原理上的知识还理解得不是很深透,因而有些知识点写得也不是很详细,只展示出来了现象,后续还需要继续努力。

举报

相关推荐

0 条评论