Contents
java反射原理
何为反射
反射
是⼤大多数语⾔言⾥里里都必不不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有)
,拿到的方法可以调⽤,总之通过“反射”,我们可以将Java
这种静态语⾔言附加上动态特性。
反射的相关方法
在了解反射的相关方法前,必须对Class
类有一个较为客观的认识:

获取Class对象的三种方法
– Object->getClass()
– 任何数据类型(包括基本数据类型)都有一个“静态”的class属性
– 通过Class类的静态方法:forName(String className)
通过一个demo
来一一进行验证:
package learn_java;
import learn_java.Students;
public class reflect {
public static void main(String args[]) throws ClassNotFoundException {
Students A = new Students("Crispr","2019111111","male",2);
//第一种获取Class的方法
Class clazz1 = A.getClass();
System.out.println(clazz1);
//通过静态属性获取Class
Class clazz2 = Students.class;
System.out.println(clazz1 == clazz2);
//通过类的静态方法获得Class
Class clazz3 = Class.forName("learn_java.Students");
System.out.println(clazz3 == clazz1);
}
}

有关反射的方法其实比较多,这里列举比较常见的几种方法:
– 获取类的方法:forName()
– 实例化对象的方法: NewInstance()
– 获取函数的方法: getMethod
– 执行函数的方法: Invoke
值得注意的是class.NewInstance()
就是调用该类的无参构造函数,有时候在进行漏洞利用时使用NewInstance()
可能会失败,原因有两方面:
- 该类没有无参构造函数
- 该类的无参构造函数是私有的
这里用一个最常用的例子进行说明:
Class cls = Class.forName("java.lang.Runtime");
cls.getMethod("exec",String.class).invoke(cls.newInstance(),"calc.exe");

此时报错的原因就是因为
Runtime
的构造方法是私有的,借用p牛关于反射的解释:这其实涉及到很常见的设计模式:“单例模式”。(有时候工厂模式也会写成类似)比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取:
public class TrainDB{
private static TrainDB instance = new TrainDB();
public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}
这样,只有类初始化的时候会执行一次构造函数,后面只能通过getInstance
获取这个对象,避免建立多个数据库连接。回到正题,Runtime类就是单例模式,我们只能通过Runtime.getRuntime()
来获取到Runtime对象。
因此修改为下面这样的payload
能够顺利执行我们的命令:
Class cls = Class.forName("java.lang.Runtime");
cls.getMethod("exec",String.class).invoke(cls.getMethod("getRuntime").invoke(cls),"calc.exe");
Method Class.getMethod(String name, Class<?>... parameterTypes)
的作用是获得对象所声明的公开方法
该方法的第一个参数name是要获得方法的名字,第二个参数parameterTypes
是按声明顺序标识该方法形参类型,因为方法是能够重载的,一个类可能存在多个同名方法,因此我们需要通过方法传入参数来确定唯一的方法。
invoke
的作用是执行方法,它的第一个参数是:
– 如果这个方法是一个普通方法,那么第一个参数是类对象
– 如果这个方法是一个静态方法,那么第一个参数是类
有趣的是invoke()
方法是有多态特性的,下面同样通过一个demo
来说明其多态特性:
import java.lang.reflect.Method;
public class invoke {
static class Animals{
public void print(){
System.out.println("Animals print()");
}
}
static class Cat extends Animals{
@Override
public void print(){
System.out.println("Cat print()");
}
}
public static void main(String args[]) throws Exception{
try {
Method animalPrint = Animals.class.getMethod("print");
Method catPrint = Cat.class.getMethod("print");
Animals animal = new Animals();
Cat cat = new Cat();
animalPrint.invoke(animal);
animalPrint.invoke(cat);
catPrint.invoke(cat);
catPrint.invoke(animal);
}catch (Exception e){
e.printStackTrace();
}
}
}

代码中,Cat类覆盖了父类Animal的print()方法, 然后通过反射分别获取print()的Method对象。最后分别用Cat和Animal的实例对象去执行print()方法。其中
animalMethod.invoke(animal)
和catMethod.invoke(cat)
,示例对象的真实类型和Method的声明Classs是相同的,按照预期打印结果;animalMethod.invoke(cat)中,由于Cat是Animal的子类,按照多态的特性,子类调用父类的的方法,方法执行时会动态链接到子类的实现方法上。因此,这里会调用Cat.print()方法;而catMethod.invoke(animal)中,传入的参数类型Animal是父类,却期望调用子类Cat的方法,因此会抛出异常。
跟进Invoke
实现的具体过程:
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
不难发现invoke()方法的实现
中主要分为两部分:访问控制检查和调用MethodAccessor.invoke()
实现方法执行。
继续跟进访问检查控制:
public static boolean quickCheckMemberAccess(Class<?> var0, int var1) {
return Modifier.isPublic(getClassAccessFlags(var0) & var1);
}
首先对该方法的修饰符进行判断,如果该方法是public
修饰,直接跳过访问控制,意味着public
修饰的方法是直接能够通过反射调用的,如果不是public
则继续进行检查控制:
void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers)
throws IllegalAccessException
{
if (caller == clazz) { // quick check
return; // ACCESS IS OK
}
Object cache = securityCheckCache; // read volatile
Class<?> targetClass = clazz;
if (obj != null
&& Modifier.isProtected(modifiers)
&& ((targetClass = obj.getClass()) != clazz)) {
// Must match a 2-list of { caller, targetClass }.
if (cache instanceof Class[]) {
Class<?>[] cache2 = (Class<?>[]) cache;
if (cache2[1] == targetClass &&
cache2[0] == caller) {
return; // ACCESS IS OK
}
// (Test cache[1] first since range check for [1]
// subsumes range check for [0].)
}
} else if (cache == caller) {
// Non-protected case (or obj.class == this.clazz).
return; // ACCESS IS OK
}
// If no return, fall through to the slow path.
slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
}
通过方法的(protected/private/package)修饰符或方法的声明类(例如子类可以访问父类的protected方法)与调用者caller之间的关系,判断caller是否有权限访问该方法。
不在继续向下深入代码,值得一说的是当我们正常使用方法时[1].method([2],[3],[4])
等价于在反射中的这种表达:method.invoke([1],[2],[3],[4])
,如果该方法时静态方法,则第一个参数是类,否则第一个参数就是类的实例化对象
因此如果对Runtime
方法的进行分步:
//通过类的静态方法获得Class
Class cls = Class.forName("java.lang.Runtime");
//通过getMethod方法获取getRuntime,因为该方法是无参数方法,所以不需参数类型来甄别
Method getRuntime = cls.getMethod("getRuntime");
//获取类的exec方法,由于exec方法有多个,需要参数来唯一确定
Method exec = cls.getMethod("exec",String.class);
//由于exec不是类的静态方法,需要类的实例化对象,以及具体的参数
exec.invoke(getRuntime.invoke(cls),"calc.exe");
写到这里,我也会觉得反射的利用条件也太局限了,难道没有空参数构造方法就不能通过反射实例化该类?
其实这也是利用反射实例化类的第二个方法:
通过类对象的getConstructor()
或getDeclaredConstructor()
方法获得构造器(Constructor)对象并调用其newInstance()
方法创建对象,适用于无参和有参构造方法
类似于getMethod()
方法一样,因为类的构造方法也支持重载,因此可能存在多个类的有参构造方法,因此getConstructor()
方法的参数是构造函数列表类型,通过该参数确定该类的唯一构造方法,获取到这个构造方法后,使用newInstance()
获取实例即可
先说另外一个经常使用来进行命令执行的类:ProcessBuilder
ProcessBuilder类是J2SE 1.5在java.lang中新添加的一个新类,此类用于创建操作系统进程,它提供一种启动和管理进程(也就是应用程序)的方法。在J2SE 1.5之前,都是由Process类处来实现进程的控制管理。
每个ProcessBuilder
实例管理一个进程属性集。它的start() 方法
利用这些属性创建一个新的 Process 实例。start() 方法可以从同一实例重复调用,以利用相同的或相关的属性创建新的子进程。
ProcessBuilder
有两个构造方法:
– public ProcessBuilder(List<String> command)
– public ProcessBuilder(String... command)
同样分步写出通过构造器实例化ProcessBuilder.start()
的demo:
Class clazz = Class.forName("java.lang.ProcessBuilder");
Method start = clazz.getMethod("start");
Constructor c = clazz.getConstructor(List.class);
Object pb = c.newInstance(Arrays.asList("calc.exe"));
start.invoke(pb);
下面学习另一种构造方法:
– public ProcessBuilder(String… command)
这涉及到java中的可变长参数:
用户若是想定义一个方法,但是在此之前并不知道以后要用的时候想传几个参数进去,可以在方法的参数列表中写参数类型或者数组名,然后在方法内部直接用操作数组的方式操作。
java可变长的形参声明格式:dataType...parameters
。
其中,省略号表示数据类型为dataType
的parameters参数个数不固定的,可为任意个。此外,在方法调用时,变长形式参数可被替换成1个、2个或多个参数。
因此对于可变长参数,java在编译过程中的处理和对数组的处理是一致的,也就是说一下两段代码在底层的实现是一样的:
public void hello(String... names){}
public void hello(String[] names){}
所以对于反射来说,当出现可变长参数时,将其视为数组类型即可,由于在调用newInstance
的时候,因为这个函数本身接收的是一个可变长参数,我们传给
ProcessBuilder
的也是一个可变长参数,二者叠加为一个二维数组,
Class clazz = Class.forName("java.lang.ProcessBuilder");
Method start = clazz.getMethod("start");
Constructor c = clazz.getConstructor(String[].class);
Object pb = c.newInstance(new String[][]{{"calc.exe"}});
start.invoke(pb);
接下来还有反射的另一个问题:某类的方法和属性等都是私有的,那能通过反射来获得吗?
答案当然是可以的,在java
中,反射的方法都对用私有和公有
,举个例子:getMethod()
获得类的公有方法,那么getDeclearedMethod()
就是获取该类的私有方法,这里用一个小demo来具体化:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class getUser {
public static void main(String args[]) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, NoSuchFieldException, IllegalArgumentException, InvocationTargetException {
/*通过class静态方法forName直接获取一个类的字节码文件对象,此时该类还是源文件阶段,并没有变成字节码文件*/
Class clazz1 = Class.forName("learn_java.user");
user userA = new user("Crispr", 19);
/*通过该类的实例获取该类的字节码对象,该类属于创建对象阶段*/
Class clazz2 = userA.getClass();
/*获取该类的有参构造器,可以通过构造器来实例化类*/
Constructor constructor = clazz2.getConstructor(String.class,int.class);
/*获取某一私有成员变量操作*/
Field filedName = clazz2.getDeclaredField("name");
filedName.setAccessible(true);
filedName.set(userA,"Crisprx");
System.out.println(filedName.get(userA));
/*获取全部成员变量*/
/*如果成员是私有的,则需要使用getDeclaredField,否则直接使用getField即可,此处由于是得到所有成员变量,使用getDeclaredFileds*/
Field[] fields = clazz2.getDeclaredFields();
for(int i = 0;i < fields.length; i++) {
fields[i].setAccessible(true);//获取属性对象后还需要打开可见权限
/*field.get(obj)获取成员变量的值,obj表示该属性对应的类的实例,即是从类的某个具体实例中获取的成员变量*/
System.out.println(fields[i].get(userA));
/*
* 接下来使用getMethod获取类方法,以及使用invoke来执行类方法
* */
Method method = clazz2.getDeclaredMethod("setName", String.class);
method.invoke(userA, "Name Changed");
System.out.println(userA.getName());
}
}
}
可以知道使用field
来承接私有或者公有成员变量,公有变量则使用getField
若是私有则使用getDeclearedField
即可,不过再操作前需要使用setAccessible
设为true
,否则无法进行修改
因此我们在利用Runtime
类时也能获取其私有构造方法进行命令执行:
//通过类的静态方法获得Class
Class cls = Class.forName("java.lang.Runtime");
//获取Runtime类的私有构造方法,其构造方法是无参数的,记得setAccessible
Constructor c = cls.getDeclaredConstructor();
c.setAccessible(true);
//实例化该类
Object runTime = c.newInstance();
//获取exec方法进行利用
Method exec = cls.getMethod("exec",String.class);
//使用实例来调用该方法
exec.invoke(runTime,"calc.exe");

在结尾仍然需要强调的是,当获取类的私有属性,包括方法、成员等,如果要对其进行操作(修改、调用)等,都需要使用setAccessible
将权限打开,否则无法进行调用
因为在调用获取私有成员方法
时(Invoke)时:
void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers)
throws IllegalAccessException
{
if (caller == clazz) { // quick check
return; // ACCESS IS OK
}
Object cache = securityCheckCache; // read volatile
Class<?> targetClass = clazz;
if (obj != null
&& Modifier.isProtected(modifiers)
&& ((targetClass = obj.getClass()) != clazz)) {
// Must match a 2-list of { caller, targetClass }.
if (cache instanceof Class[]) {
Class<?>[] cache2 = (Class<?>[]) cache;
if (cache2[1] == targetClass &&
cache2[0] == caller) {
return; // ACCESS IS OK
}
// (Test cache[1] first since range check for [1]
// subsumes range check for [0].)
}
} else if (cache == caller) {
// Non-protected case (or obj.class == this.clazz).
return; // ACCESS IS OK
}
// If no return, fall through to the slow path.
slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
}
都会进行checkAccess
方法,而如果是私有方法,则modifier
肯定是private
,而checkAccess
分为慢检测和快检测,在慢检测中会调用
Reflection.ensureMemberAccess(caller, clazz, obj, modifiers);
最终如果modifiers
仍然是private
,则不会调用成功:

而如果使用
setAccess(true)
后:
可以看到
obj.override
赋值为true
,因此在执行方法过程中会直接跳过检查访问控制而执行: