JAVA反射及其相关安全问题

作者: const27 分类: All,java安全,开发-JAVA 发布时间: 2020-12-10 18:52

反射的基础

反射是java的一个特性,用于获取类的详细信息(方法,变量),并可以执行类中的方法。

获得一个类的类对象

要获取类的详细信息或执行其中的方法,首先肯定是要获取到那个类的类对象

方法一:我们需要创建一个Class类型的变量,用于接收Class.forname(“类”)返回的类对象。这个方法必须通过try..catch 来处理其中ClassNotFoundException

try{
Class clazz = Class.forname("java.lang.String");}
catch(ClassNotFoundException e){}

方法二:我们实例化一个类的对象出来,然后通过 对象.getClass()获得其类对象

String a = new String;
Class clazz = a.getclass();

方法三:使用.class

Class clazz = String.class;

这样,我们就获得一个指定类的类对象了。很简单。我们可以对一个类对象使用getName()方法获取其类名。

System.out.println(clazz.getName());

获取一个类对象的所有成员

获取属性并修改

对类对象使用 getFields()  或 getDeclaredFields() 方法即可获得属性数组,区别在于前者只能获取公有属性,后者能获取私有属性。
然后遍历属性数组,通过对每一项执行getName()获取属性名,get(实例化的对象)获得属性值
然后对某一项进行.set()对其进行修改
这里需要提的是,如果要获得或修改私有属性的值的时候,需要对私有属性使用setAccessible(true)来实现私有访问


import java.lang.reflect.Field;

public class test {
    public static void main(String[] args){
        Class clazz = (new abc()).getClass();
        abc test = new abc();
        Field[] Fields = clazz.getDeclaredFields();
        for(Field Field:Fields){ //遍历Fields数组
            try { //执行get()方法时需抛出IllegalAccessException错误
                Field.setAccessible(true);  //对数组中的每一项实现私有访问
                System.out.print(Field.getName());
                Object value = Field.get(new abc());
                System.out.println(":" + value);
                Field.set(test,"new");   //修改test对象中的变量
            }
            catch (Exception e){

            }
        }
        System.out.println(test.a);
    }

}

class abc{
    public String a =new String("tom and mary");
    private String b = new String("abcdefg");
}

运行结果

另外,你也可以通过getFiled(“属性名”)来获取特定属性的Filed对象, getDeclaredFields() 同理

获取方法并执行

接下来是获取方法的一些信息
我们通过getMethod获取方法对象,然后通过getName获取方法名,getReturnType获取返回值类型,getParameterTypes获取传入的参数类型

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class test {
    public static void main(String[] args){
        Class clazz = (new abc()).getClass();
        Method[] methods = clazz.getDeclaredMethods();
        for(Method method:methods){
            try{
                System.out.print(method.getName());
                System.out.print("|retrunType:"+method.getReturnType()+"|");
                Class[] ParameterTypes = method.getParameterTypes();
                for(Class ParameterType:ParameterTypes){
                    System.out.print("ParamType:"+ParameterType.getName());
                }
                System.out.println("");
            }
            catch (Exception e){

            }
        }
    }
}

class abc{
    public int a =1;
    private String b = new String("abcdefg");

    public void func1(int input1){
        System.out.print(input1);
    }
    private void func2(int input1){
        System.out.print(input1);
    }
}

接下来是对method对象执行invoke方法调用,通过传入参数,指定对象,即可调用该方法。我在上述代码中添加了修改

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class test {
    public static void main(String[] args){
        Class clazz = (new abc()).getClass();
        Method[] methods = clazz.getDeclaredMethods();
        for(Method method:methods){
            try{
                System.out.print(method.getName());
                System.out.print("|retrunType:"+method.getReturnType()+"|");
                Class[] ParameterTypes = method.getParameterTypes();
                for(Class ParameterType:ParameterTypes){
                    System.out.print("ParamType:"+ParameterType.getName());
                }
                System.out.println("");
                method.setAccessible(true);  //打开私有访问
                method.invoke(new abc(),2);   //new 这里invoke第一个参数指定实例化对象,之后的参数代表传入方法的参数
            }
            catch (Exception e){

            }
        }
    }
}

class abc{
    public int a =1;
    private String b = new String("abcdefg");

    public void func1(int input1){
        System.out.print(input1);
    }
    private void func2(int input1){
        System.out.print(input1);
    }
}

获取构造方法并执行

获得构造方法,主要是通过 getConstructors()和getDeclaredConstructors() ,后者能访问私有对象,下面是用遍历法获得所有构造函数信息

import java.lang.reflect.Constructor;

public class test {
    public static void main(String[] args){
        try{
        Class clazz = Class.forName("abc");
        Constructor[] conArray = clazz.getDeclaredConstructors();
            for (Constructor a:conArray){
                a.setAccessible(true);
                System.out.println(a);
            }
        }
        catch(Exception e){
        }
    }
}

class abc{
    public abc(String name,int age){
        System.out.println("姓名:"+name+"年龄:"+ age);
    }
    private abc(int age){
        System.out.println("私有的构造方法   年龄:"+ age);
    }

}

运行结果

我们刚才提过,getFiled()附带参数可以指定访问某一个属性,同理,getConstructor也一样,我们想要执行某一个类的构造方法,往往这个方法更实用
要执行一个类的构造方法,那我们需要创建一个该类的实例化对象,这个过程我们用newInstance()方法实现

import java.lang.reflect.Constructor;

public class test {
    public static void main(String[] args){
        try{
        Class clazz = Class.forName("abc");

        Constructor test = clazz.getDeclaredConstructor(int.class);  //指定参数,即可从多个重载的构造函数指定到某个具体的构造函数
        test.setAccessible(true);
        test.newInstance(12);  //私有构造方法只需调用一下newInstance传入参数即可

        Constructor test2 = clazz.getDeclaredConstructor(String.class,int.class);
        test2.newInstance("tom",15);  //公有构造方法也一样
        }
        catch(Exception e){
        }
    }
}

class abc{
    public abc(String name,int age){
        System.out.println("姓名:"+name+"年龄:"+ age);
    }
    private abc(int age){
        System.out.println("私有的构造方法   年龄:"+ age);
    }

}

执行结果

反射的进阶与安全

Class.forName 实质与类初始化

Class.forName(“…”) 常被我们拿来获得类对象,但是实际上,Class.forname有三个参数,只不过我们默认输第一个参数:类名就能完成工作了。

这是Class.fornName函数原型
public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)

Class.forName(className)
// 等于
Class.forName(className, true, currentLoader)

这里我们看见有三个参数,第一个参数是指定类名就不多讲了,第二个参数是决定类是否初始化(这个稍后会详细阐明),第三个是ClassLoader类加载器(告诉JVM如何加载这个类这里不展开说)

关于一个类的初始化,有三种操作可以实现:构造方法,空块和static块,就像这样

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

那么执行顺序是如何呢,在引入父类的情况下又是如何呢?我们写个demo看看

import java.lang.reflect.Constructor;

public class test {
    public static void main(String[] args){
        abc a = new abc();
    }
}




class b{
    {
        System.out.println("b空块已执行");
    }

    static{
        System.out.println(" b static块已执行");
    }
    public b(){
        System.out.println("b类构造方法已执行");
    }
}


class abc extends b{
    {
        System.out.println("a空块已执行");
    }

    static{
        System.out.println(" a static块已执行");
    }

    public abc() {
        System.out.println("a初始化方法已执行");
    }
}

我们可以清晰的看到执行顺序,对继承类来说,
1.会先执行父类static块
2.执行自己的static块
3.执行父类空快
4.执行父类构造方法
5.执行自己空快
6.执行自己构造方法

对于一个类来说,执行顺序则是
1.执行static块
2.执行空快
3.执行构造方法

那么对于Class.forName指定的是否进行类初始化参数,指的是哪个部分?static块,空块还是构造方法?答案是只会执行static块里的,且会优先执行父类的static块。
我们把上面代码中的主函数替换为

        try {
            Class clazz = Class.forName("abc");
        }
        catch (Exception e){}

执行一下,看结果。发现只执行了static块

也就是说,Class.forName()会默认执行类的static代码块,是个比较危险的信号。

Runtime执行命令解析

一般来说,我们调用Runtime类来执行命令时的指定是这样的

Runtime.getRuntime().exe()

我们到源码里分析这段代码

可见,当我们对Runtime类执行getRuntime()时会得到一个Runtime对象,然后我们就可以调用我们的exec方法了
同时,Runtime() 被private修饰符修饰了,这说明我们无法通过Runtime a = new Runtime()来实现一个Runtime的对象。
所以说,我们正常地使用runtime来执行命令只能依靠以上代码。

那么我们要是想要依靠反射来写一个Runtime执行任意命令的payload,那么该如何写呢?

先来一个错误示范,当我们用常规的思路去实现时。我们直接调用Runtime类里的exec方法,然后通过newInstance来实例化一个Runtime对象

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

最终结果则是报错:class test cannot access a member of class java.lang.Runtime (in module java.base) with modifiers “private”
看来通过反射也不能直接调用exec方法,或者说不能实例化Runtime对象。
那么正确思路该是什么呢?应该是先调用getRuntime获得Runtime对象,然后再调用exec方法

            Class clazz = Class.forName("java.lang.Runtime");
            Method MgetRuntime = clazz.getMethod("getRuntime");
            Object runtime = MgetRuntime.invoke(clazz);  //对类对象使用getRuntime(),其实就相当于Runtime.getRuntime()。这种方式仅限static方法
            Method Mexec = clazz.getMethod("exec", String.class);
            Mexec.invoke(runtime,"calc.exe");

最终弹出计算器。

ProcessBuilder执行命令的反射实现

除了Runtime以外,还可以用ProcessBuilder类来执行命令。
它的正常情况使用如下.

ProcessBuilder pb = new ProcessBuilder("calc.exe");
pb.start();

我们来分析一下ProcessBuilder的构造方法,它的构造方法有很多个重载,我们分析一个吧。

可见,ProcessBuilder的构造方法把传入参数保存到了command属性里,然后commad会被以系统命令调用(这部分代码就不贴出来了)。

那么以反射的形式该如何实现呢。

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

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

4 × 3 =

标签云