白袍的小行星

Java安全基础——反射

字数统计: 1.5k阅读时长: 5 min
2021/05/28 Share

Java安全基础——反射

之所以要再回过头来写基础,原因是学到后面发现基础知识的重要性超乎想象。如果对这些基础知识理解不到位,那么就很难理解一些利用链或利用方法,所以必须对其熟稔于心。
Java反序列化漏洞的绝大部分利用方法都需要依托Java语言的动态特性,最著名的就是反射。反射其实就是动态地获取任意类、任意对象的信息,这个动态指的是只需要改变代码里变量的值,代码功能就会发生改变。
每个类都有一个Class对象,此对象实际是保存在一个同名的class文件中。在进行实例化时,JVM会使用ClassLoaderclass文件加载到内存中。
这里就涉及到ClassLoader的知识,我们慢慢进行。

ClassLoader

ClassLoader即类加载器,主要的功能是将字节码文件加载至内存中。Java自带了三个加载器,分别是:

  • Bootstrap ClassLoader,引导类加载器

  • Extension ClassLoader,扩展类加载器

  • App ClassLoader系统类加载器,也是默认的类加载器

    Bootstrap ClassLoader会进行核心类库的加载,主要是lib目录下的rt.jarresources.jar等。此类是在JVM层实现的,所以如果使用java.io.File.class.getClassLoader()去获取其加载器对象,会获得一个null值,因为java.io.File类已经被Bootstrap ClassLoader加载过,获取所有被Bootstrap ClassLoader加载的类的加载器对象时都会获得null.

加载类的方式不止一种,使用关键字new是一种,使用反射或ClassLoader也是一种。使用后者来加载Test类:

this.getClass().getClassLoader().loadClass("Test");

这里参数是完整的类名,包括了路径。
这种方式默认不会初始化类,注意,这里说的初始化类,而不是初始化实例。
类初始化开始之前,会将类的静态变量分配内存并置零。
类初始化时,会将类初始化代码和static{}域的代码收集为 <cinit>()方法,子类初始化时会先调用父类的<cinit>()的方法。
所以,如果有这样一个类:

public class Test{
{
System.out.println("1");
}

static{
System.out.println("2");
}

public Test(){
System.out.println("3");
}
}

这三者的调用顺序是2->1->3.static域的代码运行最早,而{}的部分是在构造方法之前调用的,最后是构造方法。如果构造方法中有super(),那么{}部分会在其后运行,但是仍早于构造方法。

反射

说完了ClassLoader部分,我们进入到反射部分。首先来看获取Class对象的方法,一般有三种:

  • obj.getClass(),使用某实例的getClass()方法,可以直接获取到它的Class对象
  • Test.class,某类的class属性也就是它的Class对象
  • Class.forName,使用Class类的forName方法,需要知道类名

forName方法有两个重载:

  • Class forName(String name)
  • Class forName(String name, boolean initialize, ClassLoader loader)
    前者不用多说,主要是后者,其中的第二个参数,代表是否进行类初始化,也就是前面提到过的步骤,而第三个参数是指定的ClassLoader.

直接获取Class对象的意义在于不需要进行import也能加载任意类,这就避免了攻击时无类可用的情况。

通过Class对象,进而可以获取Constructor类对象、Method类对象、Field 类对象:

package javasec;
public class Test {
public String t = "Hello Test";
public void echoTest(String name){
System.out.println(name);
}
}


Class c = Class.forName("javasec.Test");
//获取指定构造函数
Constructor c1 = c.getConstructor(String.class);
//获取所有构造函数
Constructor c2 = c.getConstructors();
//获取所有构造函数,不包括继承的
Constructor c3 = c.getDeclaredConstructors();

//获取指定方法
Method m1 = c.getMethod("echoTest", String.class);
//获取所有方法
Method[] m2 = c.getMethods();
//获取所有方法,不包括继承的
Method[] m3 = c.getDeclaredMethods();

//获取指定属性
Field f1 = c.getField("t");
//获取所有属性
Field[] f2 = c.getFields();
//获取所有属性,不包括继承的
Field[] f3 = c.getDeclaredFields();

这是通过反射调用方法来进行操作,也可以对当前类进行实例化,再调用想要的方法。
如果通过后者,Class对象有一个newInstance()方法,即调用此类的无参构造方法,这里就存在问题,比如:

  • 此类没有无参构造方法
  • 构造方法是私有的

这时候调用newInstance()方法就会失败。私有构造方法是设计思想的一种,即单例模式,在静态属性赋值时进行实例化,之后需要获取对象时通过一个静态方法完成,所以解决办法也是使用getMethod()来调用这个静态方法。

执行方法使用Method.invoke()方法,第一个参数根据所调用方法的性质来确定,如果是静态方法,第一个参数就是类,否则就是类对象。

我们试着用反射的方式来进行命令执行:

//获取Runtime类的Class对象
Class runtimeClass = Class.forName("java.lang.Runtime");
//获取相应的方法
Method method = runtimeClass.getMethod("exec", String.class);
Method runtimeMethod = runtimeClass.getMethod("getRuntime");
//调用方法
Object obj = runtimeMethod.invoke(runtimeClass);
method.invoke(obj, "calc.exe");

如果不存在无参构造方法,也没有单例模式的静态方法,就需要用getConstructor方法去获取其他构造方法,例如要使用ProcessBuilder类来进行命令执行,这个类没有无参构造方法,所以:

Class processClass = Class.forName("java.lang.ProcessBuilder");
Method method = processClass.getMethod("start");
Constructor constructor = processClass.getConstructor(List.class);
Object obj = constructor.newInstance(Arrays.asList("/Users/adan0s/test.sh"));
method.invoke(obj);

如果构造方法为私有,可以使用getDeclaredConstructor方法获取它,再使用setAccessible方法修改权限,继而就能调用了:

Class runtimeClass = Class.forName("java.lang.Runtime");
Constructor constructor = runtimeClass.getDeclaredConstructor();
constructor.setAccessible(true);
Method method = runtimeClass.getMethod("exec", String.class);
method.invoke(constructor.newInstance(), "calc.exe");

参考

主要参考了phith0n师傅的Java安全漫谈系列,以及一些网上讲Java相关知识的文章,不一一列出了。

CATALOG
  1. 1. Java安全基础——反射
  2. 2. ClassLoader
  3. 3. 反射
  4. 4. 参考