0x00 什么是反射
反射是Java的特征之一,可以在运行时检查类、接口、方法和变量等信息,无需知道类的名字,方法名等。
对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以进行调用。
反射的应用十分广泛,比如在编译器中,输入一个对象或类然后编译器会自动列出它的属性或方法,就是通过反射实现的。
还有在spring
、mybatis
等框架中也使用了反射来实现。
但是正因为反射太过灵活,如果代码编写不当,就很有可能出现一些安全问题。在一些漏洞利用中,利用反射来进行命令执行也是很常见的。因此不管是漏洞利用,还是代码审计,学习java安全绕不开反射的学习。
0x01 反射的实现
0x00 获取类对象
想要使用反射,首先得有一个类,必须获取到java.lang.Class
对象,这里有几种方法获取。
0x00 forName()
forName()
方法只要有一个类名称,就可以获得一个类对象。
例如如下代码
Class<?> clazz = Class.forName("java.lang.Runtime");
System.out.println(clazz);
就会输出class java.lang.Runtime
,拿到了一个类对象。
在配置jdbc
的时候就是通过Class.forName("com.mysql.jdbc.Driver")
来进行配置的。
0x01 getClass()
如果在上下文中,已经存在了我们想要的那个类的实例,那我们可以直接通过getClass()
来获取字节码对象。
Runtime runtime = Runtime.getRuntime();
Class<?> clazz = runtime.getClass();
System.out.println(clazz);
上述代码同样也会得到class java.lang.Runtime
。
0x02 直接获取
如果已经加载了了某个类,只是想获取到它的java.lang.Class
对象,那么就直接拿它的class 属性即可。
Class<?> clazz = Runtime.class;
System.out.println(clazz);
同样也会得到class java.lang.Runtime
。
0x03 getSystemClassLoader().loadClass()
与forName()
类似,getSystemClassLoader().loadClass()
也可以通过类名来获取类对象,但是不同的是,forName()
的静态方法JVM会装载类,并且执行static{}
中的代码,而getSystemClassLoader().loadClass()
则不会。比如说存在一个类Test
,内容如下
public class Test {
static {
System.out.println("static");
}
}
测试代码如下,分别使用二者来获取类对象。
public static void main(String[] args) throws Exception {
System.out.println("getSystemClassLoader:");
Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("Test");
System.out.println(clazz);
System.out.println("forName:");
clazz = Class.forName("Test");
System.out.println(clazz);
}
输出如下,可以看到forName()
执行了static{}
中的代码,而getSystemClassLoader().loadClass()
并没有。
getSystemClassLoader:
class Test
forName:
static
class Test
0x01 获取类方法
0x00 getMethod()
getMethod()
方法可以返回一个特定的public
类型的方法,包括其继承的方法,第一个参数为要返回的方法名,后面的参数为要返回的参数类型。getMethod()
方法的传入参数类型为getMethod(String name, Class<?>... parameterTypes)
,后面是一个可变长参数,所以我们也可以将所有参数类型作为一个数组传给getMethod()
。
比如说这里我们有一个Test
类,有一个方法print()
如下。
public void print(String name, int id){
System.out.println(name);
System.out.println(id);
}
我们就可以通过以下代码来获取Test
类的print()
方法。print()
方法需要两个参数,第一个参数为String
类型,第二个参数为int
类型,我们可以传入一个数组{String.class, int.class}
,也可以直接传入两个参数String.class, int.class
,二者是等价的。
Class<?>[] classArray = {String.class, int.class};
Class<?> clazz = Class.forName("Test");
System.out.println(clazz.getMethod("print", classArray));
就可以得到输出public void Test.print(java.lang.String,int)
,就拿到了Test
类的print()
方法。
getMethod()
方法只能返回一个特定的public
类型的方法,不能返回protected
和private
类型的方法,否则会报错Exception in thread "main" java.lang.NoSuchMethodException
。
0x01 getDeclaredMethod()
getDeclaredMethod()
方法与getMethod()
方法类似,但是getDeclaredMethod()
方法可以返回public
、protected
、private
和默认方法,但是不包括继承的方法。这里就不做演示了。
0x02 getMethods()
getMethods()
方法会返回某个类的所有public
方法,包括其继承的方法,与getMethod()
类似。
比如如下代码就会返回java.lang.Runtime
类中的所有public
方法。
Class<?> clazz = Class.forName("java.lang.Runtime");
Method[] methods = clazz.getMethods();
for(Method m:methods){
System.out.println(m);
}
0x03 getDeclaredMethod()
同理,getDeclaredMethod()
也类似,可以返回某个类所有public
、protected
、private
和默认方法,但是不包括继承的方法。
0x02 初始化对象
0x00 一般情况
获取到类的对象及方法后,我们想要调用方法,就得先实例化这个类对象。
class.newInstance()
的作用就是调用这个类的无参构造函数,比如说通过以下代码我们就能拿到一个Test
类的实例化对象。
Class<?> clazz = Class.forName("Test");
System.out.println(clazz.newInstance());
0x01 不存在无参构造方法
如果该类没有无参构造方法,只有一个含参的构造方法时,就可以利用getConstructor()
来进行实例化了。
比如说Test
类存在一个含参的构造方法
public Test(String name, int id) {
this.name = name;
this.id = id;
}
就可以通过如下代码来实例化,其中getConstructor()
方法的参数为所选的类的构造方法的参数类型,newInstance()
方法的参数为构造方法的参数值。
Class<?> clazz = Class.forName("Test");
System.out.println(clazz.getConstructor(String.class, int.class).newInstance("So4ms", 666));
0x02 构造方法私有
但是当我们想通过同样的方法去获取java.lang.Runtime
的实例化对象时,就会出现报错Exception in thread "main" java.lang.IllegalAccessException: class SerializeMain cannot access a member of class java.lang.Runtime (in module java.base) with modifiers "private"
这是因为它的无参构造方法为private Runtime() {}
,是一个private
类型的方法,我们只能选择通过getRuntime()
来获取到Runtime 对象。
public static Runtime getRuntime() {
return currentRuntime;
}
这样写就不会报错了。
Class<?> clazz = Class.forName("java.lang.Runtime");
System.out.println(clazz.getMethod("getRuntime").invoke(clazz);
如果想要获取的类不存在类似java.lang.Runtime
这样存在单例模式里的静态方法的话,可以通过之前提到的getDeclaredConstructor()
方法来进行获取构造方法。getDeclaredConstructor()
与getConstructor()
的关系可以和之前提到的getMethod()
与getDeclaredMethod()
进行类比。
同样,这里的getDeclaredConstructor()
方法的参数为想要获取的构造方法的参数类型,newInstance()
方法的参数为构造方法的参数值。同时还得使用m.setAccessible(true);
来对作用域进行修改。
Class<?> clazz = Class.forName("Test");
Constructor<?> m = clazz.getDeclaredConstructor(String.class, int.class);
m.setAccessible(true);
System.out.println(m.newInstance("So4ms", 666));
就可以调用私有的构造方法进行实例化了。
0x03 方法执行
0x00 invoke()
invoke()
方法,只要通过反射获取到方法名之后,就可以调用对应的方法。
invoke()
方法有两个参数,第一个参数是要调用方法的对象,即我们上面通过getMethod()
等方法获取的类对象。后面的参数是调用方法要传入的参数,invoke()
与getMethod()
一样也是使用的可变长参数,当调用的方法存在多个参数时,可选择传入多个等多个参数,也可选择传入一个数组。
比如我们来使用java.lang.Runtime
进行命令执行,就是如下步骤。
Class<?> clazz = Class.forName("java.lang.Runtime");
Runtime runtime = (Runtime)clazz.getMethod("getRuntime").invoke(clazz);
Method exec = clazz.getMethod("exec", String.class);
exec.invoke(runtime, "calc.exe");
综合在一起就是:
Class<?> clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");
Comments | NOTHING