JAVA反射及其相关安全问题
Contents
反射的基础
反射是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")));