Java反序列化漏洞之Java反射机制(1)
2023-06-15 17:10:57 # Web Security # Java Deserialization

前言

这是正式开始学习Java反序列化漏洞的第一篇,而Java反射机制是熟知Java反序列化漏洞的第一步,此系列笔记是为了自己能更好理解反序列化漏洞,也希望通过学习,自己能深层了解漏洞成因、学会利用、自己编写、改编利用工具。

Java Reflection

什么是Java反射机制

Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。

通俗的来说,就是我们可以通过Java的反射机制来获取到任意一个Class、变量、method、instance等等,而我们动态地任意获取Class,正有利于我们实现反序列化漏洞的利用。

静态语言和动态特性

简单来说,动态语言可以改变一个变量的类型 - 你不用提前定义某个变量的类型,比如PythonPHP,这些语言会在运行时自动探针你的变量类型,而你也可以在代码中随时对这些变量类型进行改变。

而静态语言例如Java,C/C++,C#就不一样,我们必须事先指定变量类型是String,int,还是double

而在Java中,有一个反射机制,它可以为我们提供一些动态特性 - 即使Java是一门静态语言。

Java的反射机制可以让我们做到如下:

  • 在程序运行时,查找到一个Object所属的Class
  • 在程序运行时,能找到任意一个Class的variablemethod
  • 在程序运行时,可以构造任意一个Classinstance(Object)
  • 在程序运行时,可以调用任何一个Object的方法

Class类

先来看一张图:

image-20211126201028486

Class类: 保存类信息的类

每一个新的class在创建的时候,都会new一个新的Class类。

比如,我们在创建一个String类,这个String类在创建的同时,会有一个Class cls=new Class(String)被创建,专门用来保存这个String Class。

再举个例子,比如我们在创建一个Person类,这个Person类在创建的时候,会有一个Class cls = new Class(Person)这样的instance被创建。但Class类和这两个类并非继承关系!!

所以,我们需要区分的是Class类是一个类,就像Person和String类一样,都是,Person和String类在生成的时候,都会先生成Class类的instance(对象)。

在Class类中,我们含有各种方法函数,其中Class类的构造方法是private,Class类还拥有getMethod,invoke这样的方法。

从图中可以看出:我们可以通过一个【person instance】.getClass()获取到person的Class类对象

通过【person的Class类对象】.class获取到person instance.

利用反射机制获取Class及Class静态初始化

我们知道有三种方法可以获取到一个Class对象

  • obj.getClass();

    • 当我们知道obj instance对象时可以使用这个方法
  • Class.forName("Class的名字");

  • Class.forName( String className , Boolean initialize , ClassLoader loader );

    • 此处Class.forName("Class的名字");等同于Class.forName( "Class的名字" , true , currentLoader );

      • String className : 类名
      • Boolean initialize : 是否进行类初始化
      • ClassLoader loader : 加载器( 告诉 Java 虚拟机如何加载获取的类 , Java 默认根据类名( 即类的绝对路径 , 例如 java.lang.Runtime() )来加载类 )
    • Class初始化的时候,会自动执行static{}中的代码,如果我们能够控制一个Class,并向其中添加含有恶意代码的静态代码块,当Class被初始化的时候就会执行恶意代码。

    • 当我们知道Class名字时可以使用这个方法

  • className.class

    • 当我们已经加载过某个Class,可以用这个方法

其中,Class.forName("Class的名字");是最为常用的一种方式。

1
2
3
Class<?> person = Class.forName("Person");

System.out.println(person);

利用反射机制获取method

我们在通过上面的代码获取到class后,可以获取到该Classmethod

需要注意的是,我们依然有两种不同的方法来获取到method

1
2
3
4
5
6
//第一种方式:
person.getDeclaredMethods();//获取所有Method,除了继承类的方法
person.getDeclaredMethod("methodName",parameterTypes);//获取指定的method
//第二种方式:
person.getMethods();//获取类方法,但不包含private属性的方法
person.getMethod("methodName",parameterTypes);//获取指定名字的方法

构造instance

获取到了Class,我们又该如何获取到一个新的object呢?

假设在Person Class中,我们有三个构造函数:

1
2
3
4
5
6
7
8
9
10
public Person(){//无参构造
System.out.println("NoArgConstructor");
}
public Person(String name,String age){//两个参数构造
System.out.println("TwoArgsConstructor");
}
public Person(String name,String age,boolean flag){
//三个参数构造
System.out.println("ThreeArgsConstructor");
}

当我们依次获取时

1
2
3
person.newInstance();//在创建新的instance时,class的constructor会被执行,而此处默认执行无参数constructor
person.getConstructor(String.class,String.class).newInstance("leihehe","21");//此处执行两个参数的constructor
person.getConstructor(String.class,String.class,boolean.class).newInstance("leiheee","20",true);//此处执行三个参数的constructor

可得如下结果

image-20210721212917832

invoke方法

当我们构造出来了一个新的instance(Object)且得到需要的method后,我们该如何call这个Class的方法呢?

1
2
Object o = person.newInstance();//得到object
person.getDeclaredMethod("setAge", String.class).invoke(o,"11");//call setAge()方法

补充

java.lang.Runtime命令执行及访问private的方法

我们在反序列化漏洞中会遇到的java.lang.Runtime是执行命令的最常见的方式。

以下是java执行命令的语句

1
Runtime.getRuntime().exec("ipconfig");

那如果我们要用java的反射机制来执行这段代码应该如何操作呢?

于是我写下了这段代码:

1
2
3
4
Class<?> runTimeClass = Class.forName("java.lang.Runtime");//先找到Runtime这个Class的Class类
Method exec = runTimeClass.getDeclaredMethod("exec", String.class);//找到exec这个method
Object o1 = runTimeClass.newInstance();//创建一个新的Runtime的实例
exec.invoke(o1,"ipconfig");//在实例中call exec()这个method

但是奇怪的事情发生了:

image-20210721214436241

代码提示19行出错 - 我们不能访问private属性的成员。难道是constructor有问题吗?

我们尝试进入Runtime Class中看一下:

image-20210721214751377 果然,该构造方法是private的,加上前面所提到的,className.newInstance()是直接使用无参构造,所以我们不能直接创建这个instance

如何解决呢?我们有两个方法

  1. getRuntime()

    其实之前我们就有写到执行语句是Runtime.getRuntime().exec("ipconfig");此处我们就没有new一个新的instance,反而是直接用getter来获取到Runtime

    所以我们在此处可以这样写:

    1
    2
    3
    4
    5
    Class<?> runTimeClass = Class.forName("java.lang.Runtime");//先找到Runtime这个Class
    Method exec = runTimeClass.getDeclaredMethod("exec", String.class);//找到exec这个method
    Method getRuntime = runTimeClass.getDeclaredMethod("getRuntime");//找到getRuntime()这个method
    Object o1=getRuntime.invoke(null);//call getRuntime()来获取到Runtime实例
    exec.invoke(o1,"ipconfig");//执行方法
  2. setAccessible() - 访问private方法

    Java会对private的方法进行禁止访问的操作,而setAccessible可以让我们禁止或允许Java语言的访问检查,当我们设置setAccessible(true)时,我们便可以访问private方法。

    1
    2
    3
    4
    5
    6
    Class<?> runTimeClass = Class.forName("java.lang.Runtime");//先找到Runtime这个Class
    Method exec = runTimeClass.getDeclaredMethod("exec", String.class);//找到exec这个method
    Constructor<?> declaredConstructor = runTimeClass.getDeclaredConstructor();//不带参数的getConstructor是会获取到无参构造方法的,但因为Runtime的Constructor是private的,所以我们需要使用Declared
    declaredConstructor.setAccessible(true);//禁止java语言访问检查,让我们可以访问这个私有的constructor
    Object o1 = declaredConstructor.newInstance();//通过该constructor来创建新的instance
    exec.invoke(o1,"ipconfig");//完成命令执行

java.lang.ProcessBuilder命令执行

知道我们是通过exec()执行代码后,我们接下来想要弄清楚,exec()是怎么工作的。在尝试跟随exec()方法后,我们最终来到了如下地方:

image-20210721221330679

可见我们创建了一个ProcessBuilderinstance,同时向其中传入了我们需要执行的命令cmdArraycmdArray也是以String[]的类型传入的。

进入Processbuilder可以看到它有两个constructor,一个是传入有参数的(List<String>类型),一个是传入无参数的(String[]类型),注意,这里String...等价于String[]

image-20210721222648669

再看start()方法

image-20210721221932382

传入的command又会被转化为Array,最后开始创建子程序执行等等(就不深入探究了)

综上所述,我们只需要执行new ProcessBuilder.start()即可完成命令执行

于是我们写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Class<?> processBuilderClass = Class.forName("java.lang.ProcessBuilder");//获取到ProcessBuilder这个class
Method start = processBuilderClass.getDeclaredMethod("start");//获取到start()这个方法
//因为两个constructor都是public的,所以我们可以只用用getConstructor而不用getDeclaredConstructor
Object oWithArg = processBuilderClass.getConstructor(List.class).newInstance(Arrays.asList("ipconfig","/all"));//获取带参数的命令的instance
Object oNoArg = processBuilderClass.getConstructor(String[].class).newInstance((Object) new String[]{"ipconfig"});//获取不带参数的命令的instance
Process argStart=(Process) start.invoke(oWithArg);
Process noArgStart=(Process) start.invoke(oNoArg);
InputStream argIn = argStart.getInputStream();//获取Process的输出流来作为输入字节流
InputStream noArgIn = noArgStart.getInputStream();//获取Process的输出流来作为输入字节流
InputStreamReader argReader=new InputStreamReader(argIn);//把字节流转化为字符流
InputStreamReader noArgReader=new InputStreamReader(noArgIn);//把字节流转化为字符流
BufferedReader argBr=new BufferedReader(argReader);//为字符流提供缓冲区,以便一次性读取整块数据
BufferedReader noArgBr=new BufferedReader(noArgReader);//为字符流提供缓冲区,以便一次性读取整块数据

String line=null;
while((line=argBr.readLine())!=null){//一行行读取
System.out.println(line);
}

while((line=noArgBr.readLine())!=null){//一行行读取
System.out.println(line);
}

image-20210721232833104

Conclusion

  • Java反射机制是Java这个静态语言中的动态特性,它让我们能够获取到、执行任意类的方法、变量,也能创建任意类的对象。

  • Class被初始化时会自动执行static{}中的内容,此处可以被利用。

  • Declared关键字让我们可以获取处继承类以外的所有方法。

  • setAccessible(true)可以访问private方法,不被java语言进行访问检测。

  • 我们可以通过Java反射机制利用Runtime和ProcessBuilder执行命令

Reference

Epicccal - Java 反序列化漏洞(2) – Java 反射机制