目录

  1. 1. 前言
  2. 2. 概述
    1. 2.1. 正射
    2. 2.2. 反射
  3. 3. 反射的调用
    1. 3.1. 获取类
      1. 3.1.1. obj.getClass
      2. 3.1.2. Test.class
      3. 3.1.3. forName
    2. 3.2. 获取实例化对象
    3. 3.3. 获取类的构造器/构造方法
    4. 3.4. 获取类的属性
    5. 3.5. 获取类的方法
  4. 4. 利用Runtime类执行命令
  5. 5. 利用ProcessBuilder执行命令

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Java反射

2023/7/3 Web Java
  |     |   总文章阅读量:

前言

类似于java版的__proto__(?

虽然学校专业已经教了java的基础内容,但是对于安全方面并没有什么介绍

于是在各种ctf比赛被java题拷打了无数遍555,所以接下来开始对java安全进行一个比较系统的学习

我这里尚不清楚java的学习路线,那么就先从java反射开始学习摸索一下

据说java反序列化漏洞就是从反射开始的


概述

参考先知社区的一篇文章csdn

正射

我们在编写代码时,当需要使用到某一个类的时候,都会先了解这个类是做什么的。然后实例化这个类,接着用实例化好的对象进行操作,这就是正射

通俗点就是主动去new一个类

Student student = new Student();
student.doHomework("数学");

反射

一开始不知道我们要初始化的类对象是什么的时候,那么我们就需要运用反射,通过反射我们可以获取这个类的原型

这里可以联想到nodejs中的原型链,通过某个特定方法来调用反射以获取其原型

通俗点就是逆着正射获取一开始的类对象

以上面的代码为例:正射是Student -> student,反射就是student -> Student -> 类中的所有信息(包括成员变量,成员方法,构造器等)

因为反射就相当于来到了类的内部,所以通过反射我们能直接操作类的私有属性,并且可以操纵类的字段、方法、构造器等部分

总之对象可以通过反射获取它的类,而类可以通过反射拿到所有的方法(包括私有的),拿到的方法可以调用

于是java这种静态语言就可以拥有动态特性

一段代码,改变其中的变量,将会导致这段代码产生功能性的变化,我称之为动态特性 —— phith0n


反射的调用

反射里有几个极为重要的方法:

  • 获取类的方法:forName
  • 实例化类对象的方法:newInstance
  • 获取函数的方法:getMethod
  • 执行函数的方法:invoke

这几个方法基本上包揽了java安全中各种和反射有关的payload

获取类

obj.getClass

如果上下文中存在某个类的实例obj,那么我们可以调用obj.getClass()方法来获取它的类,即:对象.getClass()

Person p = new Person();
Class clazz = p.getClass();

注:此处使用的是Object类中的getClass()方法,因为所有类都继承Object类,所以调用Object类中的getClass()方法来获取

本地测试:

class Person03 {
    String name; // 声明姓名属性
    int age; // 声明年龄属性

    public void tell() { // 取得信息的方法
        System.out.println("姓名:" + name + ",年龄:" + age);
    }
}

public class Test {
    public static void main(String[] args) {
        Person03 per = new Person03();
        per.name = "张三";
        per.age = 30;
        per.tell();
        System.out.println(per.getClass());
    }
}

image-20230703223545012

可以看到确实返回了被调用的类的类名

(不过我寻思这里对象都有了还要反射干什么)

Test.class

调用类的class属性类获取该类对应的java.lang.Class对象,即:类名.class

这里需要导入类的包才可用,不过这个方法本质上其实不属于反射,因为此时已经加载了这个类

Class clazz = Person.class;

本地测试:

class Person03 {
    String name; // 声明姓名属性
    int age; // 声明年龄属性

    public void tell() { // 取得信息的方法
        System.out.println("姓名:" + name + ",年龄:" + age);
    }
}

public class Test {
    public static void main(String[] args) {
        Person03 per = new Person03();
        per.name = "张三";
        per.age = 30;
        per.tell();
        Class personClass = Person03.class;
        System.out.println(personClass); 
    }
}

image-20230703225046102

forName

使用Class类中的forName()静态方法,即:Class.forName(“类的全路径”)

Class clazz = Class.forName("className");	// 里面要填:类所在的包名+类名
// 等价于
Class clazz = Class.forName("className", true, currentLoader);

forName的第一个参数是类名;

第二个参数表示是否进行类的初始化,值得注意的是.class创建对Class对象的引用时不会自动初始化该Class对象,而使用forName()会自动初始化该Class对象;

第三个参数ClassLoad是一个“加载器”,告诉Java虚拟机如何加载这个类,默认是根据类名来加载类,这个类名是类完整路径java.lang.Runtime,关于这个点其实有不少漏洞利用的方法,这里先不展开

本地测试:(不知道是不是版本的原因,我这里必须throws处理错误才能使用forName)

test1.java

package test;

public class test1 {
    public static void main(String[] args) throws Exception {
        Class p = Class.forName("test.phone");
        System.out.println(p);
    }
}

phone.java(这里直接用了参考文章的代码,没有特殊说明的话接下来的测试代码phone.java部分都是这个)

package test;
public class phone {
    public String name;
    public double weight;
    public phone(){
    }
    public phone(String name,double weight){
        this.name=name;
        this.weight=weight;
    }
    public void dianyuan(){ //定义一个无返回值的方法,调用会打印"开机"
        System.out.println("开机");
    }
    public void setName(String name){ //定义一个形参为String类型的方法,调用后给name属性赋值
        this.name=name;
    }
    public String getName(){ //定义一个调用后返回name属性的值的方法
        return name;
    }
    public void setWeight(double weight){ //定义一个形参为double类型的方法,调用后给weight属性赋值
        this.weight=weight;
    }
    public double getWeight(){ //定义一个调用后返回weight属性的值的方法
        return weight;
    }
}

运行test1.java得到

image-20230704164508089


当我们获得了想要操作的类的Class对象后,可以通过Class类中的方法获取和查看该类中的方法和属性

//获取包名、类名
clazz.getPackage().getName()//包名
clazz.getSimpleName()//类名
clazz.getName()//完整类名
 
//获取成员变量定义信息
getFields()//获取所有公开的成员变量,包括继承变量
getDeclaredFields()//获取本类定义的成员变量,包括私有,但不包括继承的变量
getField(变量名)
getDeclaredField(变量名)
 
//获取构造方法定义信息
getConstructor(参数类型列表)//获取公开的构造方法
getConstructors()//获取所有的公开的构造方法
getDeclaredConstructors()//获取所有的构造方法,包括私有
getDeclaredConstructor(int.class,String.class)
 
//获取方法定义信息
getMethods()//获取所有可见的方法,包括继承的方法
getMethod(方法名,参数类型列表)
getDeclaredMethods()//获取本类定义的的方法,包括私有,不包括继承的方法
getDeclaredMethod(方法名,int.class,String.class)
 
//反射新建实例
clazz.newInstance();//执行无参构造创建对象
clazz.newInstance(222,"韦小宝");//执行有参构造创建对象
clazz.getConstructor(int.class,String.class)//获取构造方法
 
//反射调用成员变量
clazz.getDeclaredField(变量名);//获取变量
clazz.setAccessible(true);//使私有成员允许访问
f.set(实例,);//为指定实例的变量赋值,静态变量,第一参数给null
f.get(实例);//访问指定实例变量的值,静态变量,第一参数给null
 
//反射调用成员方法
Method m = Clazz.getDeclaredMethod(方法名,参数类型列表);
m.setAccessible(true);//使私有方法允许被调用
m.invoke(实例,参数数据);//让指定实例来执行该方法

接下来的部分就是对上述的方法进行分析与测试


获取实例化对象

  1. 通过class的newInstance()方法

    Class p=Class.forName("类的全路径");
    Object p1=p.newInstance();
    //这里也有另一种写法,区别是要进行强制类型转化
    Class p=Class.forName("类的全路径");
    phone p1=(phone)p.newInstance();

    本地测试

    test1.java

    package test;
    
    public class test1 {
        public static void main(String[] args) throws Exception {
            Class p=Class.forName("test.phone");
            Object p1=p.newInstance();
            System.out.println(p1);
        }
    }

    image-20230704165428251

  2. 通过constructor的newInstance()方法

    Class p=Class.forName("类的全路径");
    Constructor constructor=p.getConstructor();
    Object p1=constructor.newInstance();

    这里一开始会觉得疑惑,因为和第一种方法比起来这里只是多了个调用无参构造方法的过程

    但是因为class的newInstance()方法需要我们类中存在无参的构造方法,它通过无参的构造方法来实例化,而一旦我们类中不存在无参构造方法,那么第一种方法就不行了


获取类的构造器/构造方法

  1. 获取public类型的构造器:getConstructor(class[]parameterTypes)

    Class p=Class.forName("test.phone");
    Constructor constructor=p.getConstructor();
  2. 获取全部public类型的构造器:getConstructors()

    Class p=Class.forName("test.phone");
    Constructor[] constructor=p.getConstructors();
    // 注意这里要用数组,因为全部构造器可能并不只有一个
  3. 获取public和private类型的构造器:getDeclaredConstructor(class[]parameterTypes)

    上面的两种方法是无法获取private类型的构造方法的

    Class p=Class.forName("test.phone");
    Constructor constructor=p.getDeclaredConstructor();
  4. 获取全部类型的构造器:getDeclaredConstructors()

    Class p=Class.forName("test.phone");
    Constructor[] constructor=p.getDeclaredConstructors();

    仅测试最后一个:

    test1.java

    package test;
    
    import java.lang.reflect.Constructor;
    
    public class test1 {
        public static void main(String[] args) throws Exception {
            Class p = Class.forName("test.phone");
            Constructor[] constructor = p.getDeclaredConstructors();
            for(int i=0;i<constructor.length;i++){
            System.out.println(constructor[i]);
            }
        }
    }

    image-20230704171204629


获取类的属性

  1. 获取类的一个public类型属性:getField(String name)

    Class p=Class.forName("test.phone");
    Field f=p.getField("name");
  2. 获取类的一个全部类型的属性:getDeclaredField(String name)

    Class p=Class.forName("test.phone");
    Field f=p.getDeclaredField("weight");
  3. 获取类的全部public类型的属性:getFields()

    Class p=Class.forName("test.phone");
    Field[] f=p.getFields(); //同样要注意改成数组
  4. 获取类的全部类型的属性:getDeclaredFields()

    Class p=Class.forName("test.phone");
    Field[] f=p.getDeclaredFields(); //同样要注意改成数组

    这里就测试最后一个:(不一样应该是因为版本特性

    test1.java

    package test;
    
    import java.lang.reflect.Constructor;
    import java.text.DateFormat.Field;
    
    public class test1 {
        public static void main(String[] args) throws Exception {
            Class p=Class.forName("test.phone");
            java.lang.reflect.Field[] f=p.getDeclaredFields(); //同样要注意改成数组
            for(int i=0;i<f.length;i++){
            System.out.println(f[i]);
            }
        }
    }

    image-20230704171716744


获取类的方法

  1. 获取类的一个特定public类型的方法:getMethod(String name,class[] parameterTypes)

    Class p=Class.forName("test.phone");
    Method m=p.getMethod("setName", String.class); //要注意这里有两个参数,后面要传入的是方法形参的类型的原型,无参函数就不用填
  2. 获取类的一个特定无论什么类型的方法:getDeclaredMethod(String name,class[] parameterTypes)

    Class p=Class.forName("test.phone");
    Method m=p.getDeclaredMethod("setName", String.class);
  3. 获取类的全部public的方法:getMethods()

    Class p=Class.forName("test.phone");
    Method[] m=p.getMethods();//要注意改成数组
  4. 获取类的全部类型的方法:getDeclaredMethods()

    Class p=Class.forName("test.phone");
    Method[] m=p.getDeclaredMethods(); //同样要注意改成数组

    同样仅测试最后一种:

    test1.java

    package test;
    
    import java.lang.reflect.Method;
    
    public class test1 {
        public static void main(String[] args) throws Exception {
            Class p = Class.forName("test.phone");
            Method[] m = p.getDeclaredMethods();
            for (int i = 0; i < m.length; i++) {
                System.out.println(m[i]);
            }
        }
    }

    image-20230704172338625


利用Runtime类执行命令

Java中的一个系统类,一般会写作java.lang.Runtime,它封装了应用程序运行时的环境,并提供了一些与运行时环境相关的方法

  • exec(String cmd): 在单独的进程中执行指定的命令或程序。
  • availableProcessors(): 返回当前系统的CPU数量。
  • totalMemory()和freeMemory():返回Java虚拟机中的总内存和空闲内存。
  • addShutdownHook(Thread hook): 注册一个线程,在JVM关闭时会自动执行该线程的代码。
  • halt(int status): 终止当前JVM,并返回一个指定的状态码。

我们可以发现这里的exec()方法明显能用来进行命令执行

利用反射可以实现弹计算器(注:jdk版本要小于9)

Class p = Class.forName("java.lang.Runtime");
Constructor constructor = p.getDeclaredConstructor(); // 调用私有构造器
constructor.setAccessible(true); // 修改作用域
Method m = p.getMethod("exec", String.class); // 获取exec方法
Object o = constructor.newInstance(); // 实例化对象
m.invoke(o, "calc"); // 调用exec方法,执行calc命令

image-20230822164454262


利用ProcessBuilder执行命令

Class clazz = Class.forName("java.lang.ProcessBuilder");
 ((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();