JAVA(反射)


关于反射

 通过Class实例获取class信息的方法称为反射。Java的反射机制提供为Java工程师的开发提供了相当多的便利性,同样也带来了潜在的安全风险。反射机制的存在使得我们可以越过Java本身的静态检查和类型约束,在运行期直接访问和修改目标对象的属性和状态,极有可能给恶意代码提供可乘之机。想要揭开JAVA反序列化漏洞的秘密,就必须学习JAVA的反射机制。

获取Class

 JVM在执行过程中,每读到一个类class,就会在内存中为其创建一个Class实例,这里的Class其实也是一种类,而Class实例就是这种Class类的对象,只不过它记录的是对应类的类名包名父类实现的接口所有方法字段等信息,可以说是类的个人资料。
 比如创建了个Person类

Person Wkai =new Person()

 当JVM读取到这行时,首先会读取Person.class,然后在内存为其创建个PersonClass实例并关联起来,这个实例是由JVM内部创建的,如果我们查看源码,可以发现Class的构造方法是private,只有JVM才能创建,无法人为地创建。
所以每个Class实例都对应着一种数据类型

 ───────────────────────────┐
│      Class Instance       │──────> String
├───────────────────────────┤
│name = "java.lang.String"  │
├───────────────────────────┤
│package = "java.lang"      │
├───────────────────────────┤
│super = "java.lang.Object" │
├───────────────────────────┤
│interface = CharSequence...│
├───────────────────────────┤
│field = value[],hash,...   │
├───────────────────────────┤
│method = indexOf()...      │
└───────────────────────────┘

那么如何获取一个Class实例呢?

有三种方法
 第一种:直接通过一个class的静态变量class获取

Class cls = Person.class;

 第二种:如果已经有了一个实例变量,可以通过该实例变量的getClass()方法获取

Person Wkai =new Person()
Class cls = Wkai.getClass();

 第三种:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取

Class cls = Class.forName("java.lang.String");

小结
JVM为每个加载的classinterface创建了对应的Class实例来保存classinterface的所有信息;
 获取一个class对应的Class实例后,就可以获取该class的所有信息;
 通过Class实例获取class信息的方法称为反射(Reflection)
JVM总是动态加载class,可以在运行期根据条件来控制加载class




获取字段和值

  • 对任意的一个Object实例,只要我们获取了它的Class,就可以获取它的一切信息。

  • Class提供了以下几种方法来获取字段

    • Field getField(name):根据字段名获取某个public的field(包括父类)
    • Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
    • Field[] getFields():获取所有public的field(包括父类)
    • Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
  • 首先我们来看看getField(name)getDeclaredField(name)

    • 他们的区别是这样的

      • getDeclaredField是可以获取一个类的所有字段,getField只能获取类的public 字段.
    • 那么有人问了Field又是啥玩意

      • 具体来说Field也是一种类,只不过这种类实例化后的对象是用来记录某个字段所有的值
    • 具体用法是这样的

      • 首先我定义了一个Person类
      • 通过Person类创建了个子类Student,并且拓展了两个字段
      • 那么我通过这个student类获取一个关联它的Class实例stdClass
      • 接着通过这个实例stdClass,来获取student类的指定字段的Field实例
      • 获取方式就是getField(name)getDeclaredField(name)
public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student.class;
        // 获取public字段"score":
        System.out.println(stdClass.getField("score"));
        // 获取继承的public字段"name":
        System.out.println(stdClass.getField("name"));
        // 获取private字段"grade":
        System.out.println(stdClass.getDeclaredField("grade"));
    }
}

class Student extends Person {
    public int score;
    private int grade;
}

class Person {
    public String name;
}
  • 运行结果
public int Student.score
public java.lang.String Person.name
private int Student.grade

  • 再看看Field类

  • Field类中也定义了许多方法

    • getName():返回字段名称,例如,”name”;
    • getType():返回字段类型,也是一个Class实例,例如,String.class;
    • getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。
    • get():获取指定实例在Field实例中的值
  • 接着看看代码

public final class String {
    private final byte[] value;
}
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
  • 这里先是定义了一个String类
  • 然后调用String类Class实例里的获取字段名函数返回了一个Field类型实例
  • 通过Field类型实例的getName()方法可以获取字段名
  • 通过Field类型实例的getType()方法可以获取字段类型
  • 通过getModifiers()函数可以判断字段是public还是Private等类型的字段

  • 那么如何获取字段的值呢
  • 对于一个Person实例,我们可以先拿到name字段对应的Field,再获取这个实例的name字段的值
public class Main {

    public static void main(String[] args) throws Exception {
        Object p = new Person("Xiao Ming");
        Class c = p.getClass();
        Field f = c.getDeclaredField("name");
         f.setAccessible(true); //这里要注意通过main类是无法直接访问private类型的字段值的,通过setAccessible(true)可以设置任意访问
        Object value = f.get(p);  //这里不知道为啥返回的字段值类型得是Object
        System.out.println(value); // "Xiao Ming" 
    }

}

class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

}
  • 这里先是通过Person实例p获取Person类的Class实例
  • 然后通过Person类的Class实例获取关于字段name的Field实例
  • 再通过Field实例的get()函数来获取指定实例对应的字段值

  • 既然可以查看字段值,那么可以设置字段值吗

    • 通过Field实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。
  • 设置字段值是通过Field.set(Object, Object)实现的

  • 过程和查询字段值类似,只不过调用的方法变成了set()

public class Main {

    public static void main(String[] args) throws Exception {
        Person p = new Person("Xiao Ming");
        System.out.println(p.getName()); // "Xiao Ming"

        Class c = p.getClass();
        Field f = c.getDeclaredField("name");
        f.setAccessible(true);    //同样的修改private类型的字段值也是需要设置这一项
        f.set(p, "Xiao Hong");
        System.out.println(p.getName()); // "Xiao Hong"
    }

}

class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

}
  • 总结

    • Java的反射API提供的Field类封装了字段的所有信息:
    • 通过Class实例的方法可以获取Field实例:getField()getFields()getDeclaredField()getDeclaredFields()
    • 通过Field实例可以获取字段信息:getName()getType()getModifiers()
    • 通过Field实例可以读取某个对象的字段get(),如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。同样设置某个对象的字段set(),也要调用setAccessible(true)
    • 通过反射读写字段是一种非常规方法,它会破坏对象的封装。



调用方法

  • 上面说到Class实例可以返回一个Field的实例

  • 而Class实例除了记录字段,还记录了方法

  • 那么可不可以返回关于方法的信息,并且直接调用方法呢

  • Class类提供了以下几个方法来获取Method:

    • Method getMethod(name, Class...):获取某个public的Method(包括父类)
    • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
    • Method[] getMethods():获取所有public的Method(包括父类)
    • Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
  • 示例代码

public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student.class;
        // 获取public方法getScore,参数为String:
        System.out.println(stdClass.getMethod("getScore", String.class));
        // 获取继承的public方法getName,无参数:
        System.out.println(stdClass.getMethod("getName"));
        // 获取private方法getGrade,参数为int:
        System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
    }
}

class Student extends Person {
    public int getScore(String type) {
        return 99;
    }
    private int getGrade(int year) {
        return 1;
    }
}

class Person {
    public String getName() {
        return "Person";
    }
}
  • Method也是一种类,本身包含了返回对应类方法信息的方法

    • getName():返回方法名称,例如:”getScore”;
    • getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class;
    • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class};
    • getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。
    • invoke(指定实例, 调用方法的参数):直接调用方法,必须指定实例,除非调用的方法是静态的
    public class Main {
        public static void main(String[] args) throws Exception {
            // String对象:
            String s = "Hello world";
            // 获取String substring(int)方法,参数为int:
            Method m = String.class.getMethod("substring", int.class);
            // 在s对象上调用该方法并获取结果:
            String r = (String) m.invoke(s, 6);
            // 打印调用结果:
            System.out.println(r);
        }
    }
    • 当访问静态方法的时候,是没有实例给你指定的,所以invoke()第一个参数为null就好了
    public class Main {
        public static void main(String[] args) throws Exception {
            // 获取Integer.parseInt(String)方法,参数为String:
            Method m = Integer.class.getMethod("parseInt", String.class);
            // 调用该静态方法并获取结果:
            Integer n = (Integer) m.invoke(null, "12345");
            // 打印调用结果:
            System.out.println(n);
        }
    }
    • 类似的,存在私有变量,同时也存在私有方法,访问私有方法的时候同样需要设置setAccessible(true)
    public class Main {
        public static void main(String[] args) throws Exception {
            Person p = new Person();
            Method m = p.getClass().getDeclaredMethod("setName", String.class);
            m.setAccessible(true); //不设置就会报错
            m.invoke(p, "Bob");
            System.out.println(p.name);
        }
    }
    
    class Person {
        String name;
        private void setName(String name) {
            this.name = name;
        }
    }

  • 最后再来看看这段代码
public static void main(String[] args) throws Exception {

      Object runtime=Class.forName("java.lang.Runtime") .getMethod("getRuntime",new Class[]{}).invoke(null);
    //因为Runtime类的实例是不能由我们创建的,所以需要通过getRuntime返回一个实例
      Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(runtime,"calc.exe");
    //需要指定实例,才能调用方法
  }
  • 之前一直疑惑,为啥我调用个计算器需要这么多步骤,感觉怪怪的,给人一种多此一举的感觉

  • 原来是因为Runtime类的实例必须是JVM虚拟机创建的,不能人为地创建,对比上边的Person的Method实例,可以直接创建个Person类的对象,然后invoke()马上就能用,而Runtime类还得通过getRuntime()来返回一个实例

  • 再来说说什么是Runtime类

    • 该类主要代表了应用程序的运行环境。一个Runtime就代表一个运行环境。

    • 常用的方法有:

      • getRuntime():该方法用于返回当前应用程序的运行环境对象。
      • exec(String command):该方法用于根据指定的路径执行对应的可执行文件。
参考

[1]https://www.freebuf.com/vuls/170344.html

[2]https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512


文章作者: wkai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wkai !
 上一篇
(五)SQL注入—报错注入(floor原理) (五)SQL注入—报错注入(floor原理)
这是一段通过报错来进行sql注入的payload,它的核心函数是floor() ?id=1' and (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x
2020-08-30
下一篇 
(五)SQL注入—报错注入(updatexml函数) (五)SQL注入—报错注入(updatexml函数)
 updatexml()函数主要用于修改查询到的内容,它总共需要三个参数,其中第二个参数必须要符合XPATH语法,否则将会返回参数错误,所以如果网页开启了错误回显,就可以使用这个函数来进行sql报错注入  正好知识盒子中
2020-08-26
  目录