# 前言

大佬几年前的文章还是能让我受益匪浅,这里浅记 java 反射的学习,参考了 P 牛的文章

# java 反射 (forName)

首先看看官方对反射的解释

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.
The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

java 安全从反序列化入手,从反射入门反序列化,反射是大多数语言不可或缺的部分,对象可以通过反射来获取其他的类,类可以通过反射拿到所有的成员方法 ( Methods )、成员变量 ( Fields )、构造方法 ( Constructors ),这里所有就是字面意思上的所有,包括私有类

# 类的加载机制

学习 java,不得不学习他的类加载机制,java 是依赖于 JVM 实现的跨平台语言,在运行时需要先编译成 class 文件,在初始化的时候会调用 java.lang.ClassLoader 加载类字节码这里贴一张大佬的图

image-20230325214537182

类加载分为四层,一共有四层 classloader(程序在启动的时候不会一次性加载所有的 class 文件,而是根据程序的需要,通过 Java 的类加载机制来动态加载某个 class 文件到内存中)分别为

  1. Extension ClassLoader:称为扩展类加载器,负责加载 Java 的扩展库,默认加载 JAVA_HOME/jre/bil/ext 下的所有 class 方法

  2. BootStrap ClassLoader:启动类加载器,是 Java 类加载层次顶层的类加载器,负责加载 JDK 中的核心类库,如:rt.jar、resources.jar、charsets.jar

    package lq;
    import java.net.URL;
    public class test {
        public static void main(String[] args){
            URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
            for (int i=0 ;i<urls.length;i++){
                System.out.println(urls[i].toExternalForm());
            }
        }
    }

    通过上面的代码,获取到该类加载器从哪些地方加载了相关的 jar 和 class 文件

    image-20230321205954293

  3. App ClassLoader: 系统类加载器。负责加载应用程序 classpath 目录下的所有 jar 和 class 文件

  4. Custom ClassLoader:上面三个是 java 默认提供的 classloader,用户可以根据需要自定义自己的 ClassLoader 文件

# 原理介绍

ClassLoader 使用的是双亲委派模型来搜索类,每一个 ClassLoader 实例都有一个父类加载器的引用,当需要搜索某个类的时候,这个过程是从上至下的,首先 Bootstrap ClassLoader 尝试加载如果没有加载到,交由 Extension ClassLoader 尝试加载,没找到交由 APP ClassLoader 尝试加载,如果还是没有加载到这返回给委托的发起者,由其指定(虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它 ClassLoader 实例的的父类加载器)这里如果都没找到会抛出异常

ClassLoader 类有如下核心方法:

  1. loadClass (加载指定的 Java 类)
  2. findClass (查找指定的 Java 类)
  3. findLoadedClass (查找 JVM 已经加载过的类)
  4. defineClass (定义一个 Java 类)
  5. resolveClass (链接指定的 Java 类)

使用这样的模型可以避免重复加载类,父类已经加载了这个类的时候,没必要子 ClassLoader 在进行一次加载

Jvm 中两个类是否相同需要同时满足两个条件

  1. 两个类名相同
  2. 两个类是由同一个 ClassLoader 实例加载的(这是因为 ClassLoader 的隔离机制)

偷一张图

image-20230321211521499

# 反射 demo

下面这样一段代码,在参数传入之前不知道他的作用是什么

public void execute(String className,String methodName) throws Exception{
        Class clazz = Class.forName(className);
        clazz.getMethod(methodName).invoke(clazz.newInstance());
    }

上面的示例中利用到了反射中尤为重要的几种方法

  1. 获取类的方法:forName
  2. 实例化类对象的方法:newInstance
  3. 获取函数的方法:getMethod
  4. 执行函数的方法:invoke

上面包揽了 Java 中和反射有关的常用方法

这里记录一下 new 一个类对象后都干了什么

  1. 首先找到目标类文件并加载到内存中
  2. 执行该类的 static 方法(如果有的话),通过该方法给目标类进行初始化
  3. 在堆内存中开辟空间,分配内存地址
  4. 在堆内存中建立对象特有属性,并进行默认初始化
  5. 对属性进行显示初始化
  6. 对对象进行构造代码块初始化
  7. 对对象进行对应的构造函数初始化
  8. 将内存地址交付给栈内存中定义变量

forName 不是获取 “类” 的唯一途径,通常来说我们有如下三种方式获取一个 “类”,也就是 java.lang.class 对象

  • obj.getclass () 如果上下文中存在某个类的示例 obj,那么我们可以直接通过 obj.getClass () 来获取类
  • Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接拿它的 class 属性即可。这个⽅法其实不属于反射。
  • Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取

注:在一个 JVM 中,一个类只会有一个 “类对象” 存在

package demo;
public class demo {
    public String name="admin";
    private String passwd="admin123";
}

下面通过 getName 来获取类对象

package lq;
public class test {
    public static void main(String[] args){
        String classname="lq.demo";
        try{
            Class pClass1 = Class.forName(classname);
            System.out.println(pClass1);
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}
//class lq.demo

forName 有两个函数重载

  1. class<?> forName(String name)
  2. class<?> forName(String name,**boolean** initialize,ClassLoader loader)

第一种是我们常用的方法,也就是上面的示例中所使用的方法,可以理解为第二种方式的一个封装,第一个参数是类名,第二个参数标识是否初始化,第三个参数是加载器,高数 javaVM 如何加载这个类

上面既然提到初始化,这里先看一个简单的类

package lq;
public class demo2 {
    {
        System.out.printf("Empty block initial %s\n", this.getClass());
    }
    static {
        System.out.printf("Static initial %s\n", demo2.class);
    }
    public demo2() {
        System.out.printf("Initial %s\n", this.getClass());
    }
}

下面写一个获取类,运行一下上面的方法

package lq;
import jdk.internal.org.objectweb.asm.Handle;
public class test {
    public static void main(String[] args){
        String classname="lq.demo2";
        try{
            Class pClass1 = Class.forName(classname);
            Class pClass2 = demo2.class;
            Class pClass3 = new demo2().getClass();
            System.out.println(pClass2);
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}
/*
Static initial class lq.demo2
Empty block initial class lq.demo2
Initial class lq.demo2
class lq.demo2
*/

image-20230317003911738

⾸先调⽤的是 static {} ,其次是 {} ,最后是构造函数,其中, static {} 就是在 “类初始化” 的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super () 后⾯,但在当前构造函数内容的前⾯。所以说, forName 中的 initialize=true 其实就是告诉 Java 虚拟机是否执⾏” 类初始化 “。

# 反射创建对象

在正常情况下,除了系统类,如果我们想要拿到一个类,首先需要 import 才能使用,而使用 forName 就不需要,这样对于我们攻击者来说十分有利的,我们可以加载任意的类

在一些源码中或许可以看到类名的部分包含 $ 符号,它的作用是查找内部类,java 中如果一个类中编写另一个内,在编译的时候会生成两个文件,通过 Class.forName ("c1$c2") 就可以加载这个内部类

获取类对象: Class classname = Class.fromName("demo.demo");
获取构造器对象: Constructor con = class.getConstructor(形参.class);
获取对象:demo demo = con.newInstance(实参)

写一个 demo 类,添加 6 种构造方法

package demo;
public class demo {
    public String name="admin";
    public String passwd="admin123";
    // 默认
    demo(String str){
        System.out.println("(默认)的构造方法 s = "+str);
    }
    // 无参的构造方法
    public demo(){
        System.out.println("调用了公有,无参构造方法");
    }
    // 一个参数的构造方法
    public demo(char name){
        System.out.println("姓名:"+name);
    }
    // 有多个参数的构造方法
    public demo(String name,String pass){
        System.out.println("用户名:"+name+"密码:"+pass);
    }
    // 受保护的构造方法
    protected demo(boolean n){
        System.out.println("受保护 n = " + n);
    }
    // 私有构造方法
    private demo(float pass){
        System.out.println("私有的构造方法"+pass);
    }
}

通过反射机制获取对象

import java.lang.reflect.Constructor;
public class demo {
public static void main(String[] args) throws Exception{
    Class classname = Class.forName("demo.demo");
    System.out.println("---------所有公有构造方法-----------");
    Constructor[] conArray = classname.getConstructors();// 获取所有 “公有的” 获取方法
    for(Constructor c:conArray){
        System.out.println(c);
    }
    System.out.println("----------所有的构造方法(包括:私有,受保护,默认,,公有)----------");
    conArray = classname.getDeclaredConstructors();// 获取全部的构造方法
    for(Constructor c:conArray){
        System.out.println(c);
    }
    System.out.println("----------获取公有的、无参的构造方法----------");
    Constructor con = classname.getConstructor(null);// 获取单个 “公有的” 构造方法
    // 这里的 null 写不写都行
    System.out.println("Con = " + con);
    Object obj = con.newInstance();// 实例化类对象的方法
    System.out.println("----------获取私有的构造方法----------");
    con = classname.getDeclaredConstructor(String.class);// 获取构造方法,没有限制
    System.out.println(con);
    con.setAccessible(true);// 暴力访问
    obj = con.newInstance("admin123");
}
}
/*
--------- 所有公有构造方法 -----------
public demo.demo (java.lang.String,java.lang.String)
public demo.demo (char)
public demo.demo ()
---------- 所有的构造方法 (包括:私有,受保护,默认,,公有)----------
private demo.demo (float)
protected demo.demo (boolean)
public demo.demo (java.lang.String,java.lang.String)
public demo.demo (char)
public demo.demo ()
demo.demo (java.lang.String)
---------- 获取公有的、无参的构造方法 ----------
Con = public demo.demo ()
调用了公有,无参构造方法
---------- 获取私有的构造方法 ----------
demo.demo (java.lang.String)
(默认) 的构造方法 s = admin123
*/

获取构造器对象方法:

  1. 批量的方法:
    • public Constructor [] getConstructors ():所有” 公有的” 构造方法
    • public Constructor [] getDeclaredConstructors ():获取所有的构造方法 (包括私有、受保护、默认公有)
  2. 获取单个的方法:
    1. public Constructor getConstructor (Class…parameterTypes): 获取单个的” 公有的” 构造方法
    2. public Constructor getDeclaredConstructor (Class…parameterTypes): 获取” 某个构造方法” 可以是私有的,或受保护、默认、公有;

这里在记录几个常用的函数

  1. class.newInstance () 的作用就是调用这个类的无参构造函数
  2. getMethod 的作用是通过反射获取一个类的某个特定的公有方法。
  3. invoke 的作用是执行方法

# 反射 java.lang.Runtime

Runtime 没法直接 new,通过下面的方式创建这个对象

public class demo {
    public static void main(String[] args){
    Runtime runtime = Runtime.getRuntime();
    System.out.println(runtime);
    }
}
//java.lang.Runtime@1b6d3586
//1b6d3586 这个是对象在内存中地址的 16 进制

下面是常用的几种方法

  1. freeMemory ():Return JVM 的空闲内存量,以字节为单位。
  2. maxMemory ():Return JVM 试图使用的最大内存量。
  3. totalMemory ():Return JVM 中的内存总量。
  4. availableProcessors () :Return JVM 的处理器数量
  5. exit (int status): 通过启动其关闭序列来终止当前正在运行的 JVM

代码演示

public class demo {
    public static void main(String[] args){
    Runtime runtime = Runtime.getRuntime();
    System.out.println(runtime);
    System.out.println("JVM中的空闲内存量"+runtime.freeMemory());
    System.out.println("JVM试图使用的最大内存量"+runtime.maxMemory());
    System.out.println("JVM中内存总量"+runtime.totalMemory());
    System.out.println("JVM的处理器数量"+runtime.availableProcessors());
    }
}
//java.lang.Runtime@1b6d3586
//        JVM 中的空闲内存量 250001304
//        JVM 试图使用的最大内存量 3784310784
//        JVM 中内存总量 255328256
//        JVM 的处理器数量 12

这样看来 Runtime 似乎在安全方面没有太大的影响,实际上 java.lang.runtime 有一个 exec 方法可以执行本地命令,所以在很多的 payload 都会看到 Runtime 的身影,通过反射调用 Runtime 来执行本地系统命令

首先是不使用反射执行本地命令

package Clown;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class demo {
    public static void main(String[] args) throws IOException {
        // 使用 Runtime 类的 exec () 方法在本地系统上执行命令
        Process process = Runtime.getRuntime().exec("cmd /c dir");
        // 使用 BufferedReader 和 InputStreamReader 读取命令的输出
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        // 逐行读取命令的输出,并打印到控制台上
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    }
}

image-20230326140122498

然后这里通过反射来实现

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
public class demo {
    public static void main(String[] args) throws Exception{
        // 获取 Runtime 类的 Class 对象
        Class RunClass = Class.forName("java.lang.Runtime");
        // 获取 Runtime 类的 getRuntime 方法
        Method getRuntimeMethod = RunClass.getMethod("getRuntime");
        // 调用 getRuntime 方法获取 Runtime 类的实例
        Object runtimeObjetct = getRuntimeMethod.invoke(null);
        // 获取 Runtime 类的 exec 方法
        Method exec = RunClass.getMethod("exec", String.class);
        // 调用 exec 方法执行系统命令
        Process process = (Process) exec.invoke(runtimeObjetct,"cmd /c dir");
        // 获取进程的标准输出流
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        // 逐行读取命令的输出,并打印到控制台上
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    }
}

image-20230326142727290