JAVA安全-02反射机制

JAVA安全-02反射机制

什么是反射

Java反射(Reflection)是Java非常重要的动态特性,通过使用反射可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。Java反射机制是Java语言的动态性的重要体现,也是Java的各种框架底层实现的灵魂。

反射的关键就是对java.lang.Class对象操作。所以我们需要先想办法获取到Class对象,通常我们有如下几种方式获取一个类的Class对象:

  1. 类名.class,如:com.test.classloader.TestHelloWorld.class
  2. Class.forName("com.test.classloader.TestHelloWorld")
  3. classLoader.loadClass("com.test.classloader.TestHelloWorld");

注:获取类还可以obj.getClass() 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过

obj.getClass() 来获取它的类。

获取Runtime类Class对象代码片段

String className     = "java.lang.Runtime";
Class  runtimeClass1 = Class.forName(className);
Class  runtimeClass2 = java.lang.Runtime.class;
Class  runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);


通过以上任意一种方式就可以获取java.lang.Runtime类的Class对象了,反射调用内部类的时候需要使用$来代替.,如com.Test类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成:com.Test$Hello

反射调用类方法

获取当前类所有的成员方法:

Method[] methods = ClassNane.getDeclaredMethods()

获取当前类指定的成员方法:

Method method = ClassNane.getDeclaredMethod("方法名");
Method method = ClassNane.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);

getMethodgetDeclaredMethod都能够获取到类成员方法,区别在于getMethod只能获取到当前类和父类的所有有权限的方法(如:public),而getDeclaredMethod能获取到当前类的所有成员方法(不包含父类)。

获取到java.lang.reflect.Method对象以后我们可以通过Methodinvoke方法来调用类方法。

method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);

method.invoke的第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)的方式调用。

method.invoke的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型

反射调用成员变量

获取当前类的所有成员变量:

Field fields = clazz.getDeclaredFields();

获取当前类指定的成员变量:

Field field  = clazz.getDeclaredField("变量名");

getFieldgetDeclaredField的区别同getMethodgetDeclaredMethod,前者无法获取到私有方法。getDeclaredMethodsetAccessible(true)一起使用修改私有属性值。

//获取成员变量值:
Object obj = field.get(类实例对象);
//修改成员变量值:
field.set(类实例对象, 修改后的值);

Field field  = clazz.getDeclaredField("变量名");
field.setAccessible(true);//设置修改权限
field.set(类实例对象, 修改后的值);

反射java.lang.Runtime

java.lang.Runtime因为有一个exec方法可以执行本地命令,所以在很多的payload中我们都能看到反射调用Runtime类来执行本地系统命令。

不使用反射执行本地命令代码片段:

// 输出命令执行结果
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(), "UTF-8"));

反射Runtime执行本地命令代码片段:

第一种:Runtime.getRuntime()

常见的类使用的执行方式,但是Runtime不行

Class clazz = Class.forName("java.lang.Runtime"); 
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

/*
Exception in thread "main" java.lang.IllegalAccessException: Class ClassTest can not access a member of class java.lang.Runtime with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.Class.newInstance(Class.java:436)
	at ClassTest.main(ClassTest.java:14)
*/

在Java的任何一个类都必须有一个或多个构造方法,如果代码中没有创建构造方法那么在类编译的时候会自动创建一个无参数的构造方法。

public class Runtime {
  public static Runtime getRuntime() {
       return currentRuntime;
   }

  /** Don't let anyone else instantiate this class */
  private Runtime() {}
}

从上面的Runtime类代码注释我们看到它本身是不希望除了其自身的任何人去创建该类实例的,因为这是一个私有的类构造方法。

注:这里涉及到设计模式,单例模式(Singleton)的目的是为了保证在一个进程中,某个类有且仅有一个实例。Runtime类就是单例模式,所以我们没办法new一个Runtime类实例即不能使用Runtime rt = new Runtime();clazz.newInstance()的方式创建Runtime对象。

通过 Runtime.getRuntime() 来获取到 Runtime 对象。

Class clazz = Class.forName("java.lang.Runtime"); 
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "open -a Calculator");

第二种:getConstructor

// 获取Runtime类对象
Class runtimeClass1 = Class.forName("java.lang.Runtime");

// 获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
//修改方法的访问权限,同时由于JDK的安全检查耗时较多.所以通过setAccessible(true)的方式关闭安全检查就可以达到提升反射速度的目的 
constructor.setAccessible(true);

// 创建Runtime类实例,等价于 Runtime rt = new Runtime();但是不能用Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();

// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);

// 调用exec方法,等价于 rt.exec(cmd);
Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);

// 获取命令执行结果
InputStream in = process.getInputStream();

// 输出命令执行结果
System.out.println(IOUtils.toString(in, "UTF-8"));

反射调用Runtime实现本地命令执行的流程如下:

  1. 反射获取Runtime类对象(Class.forName("java.lang.Runtime"))。
  2. 使用Runtime类的Class对象获取Runtime类的无参数构造方法(getDeclaredConstructor()),因为Runtime的构造方法是private的我们无法直接调用,所以我们需要通过反射去修改方法的访问权限(constructor.setAccessible(true))。
  3. 获取Runtime类的exec(String)方法(runtimeClass1.getMethod("exec", String.class);)。
  4. 调用exec(String)方法(runtimeMethod.invoke(runtimeInstance, cmd))。

runtimeClass1.getDeclaredConstructorruntimeClass1.getConstructor都可以获取到类构造方法,区别在于后者无法获取到私有方法,所以一般在获取某个类的构造方法时候我们会使用前者去获取构造方法。如果构造方法有一个或多个参数的情况下我们应该在获取构造方法时候传入对应的参数类型数组,如:clazz.getDeclaredConstructor(String.class, String.class)

获取类的所有构造方法:clazz.getDeclaredConstructors来获取一个Constructor数组。

获取到Constructor以后我们可以通过constructor.newInstance()来创建类实例,同理如果有参数的情况下我们应该传入对应的参数值,如:constructor.newInstance("admin", "123456")。当我们没有访问构造方法权限时我们应该调用constructor.setAccessible(true)修改访问权限就可以成功的创建出类实例了。

反射java.lang.ProcessBuilder

执行命令的第二个类ProcessBuilder,使用反射来获取其构造函数,然后调用start() 来执行命令

Class clazz = Class.forName("java.lang.ProcessBuilder"); 
((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("open","-a","Calculator"))).start();


前面这个Payload用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。

Class clazz = Class.forName("java.lang.ProcessBuilder"); clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("open","-a","Calculator")));
//通过 getMethod("start") 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是ProcessBuilder Object(List)

ProcessBuilder有两个构造函数,上面传入的是list.class

    public ProcessBuilder(List<String> command) {
        if (command == null)
            throw new NullPointerException();
        this.command = command;
    }

    public ProcessBuilder(String... command) {
        this.command = new ArrayList<>(command.length);
        for (String arg : command)
            this.command.add(arg);
    }

下面使用public ProcessBuilder(String... command) 这个构造函数

Class clazz = Class.forName("java.lang.ProcessBuilder"); 
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();

反射的方法执行

Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"open","-a","Calculator"}}));

参考文章

P牛java漫谈反射篇

https://zhishihezi.net/b/5d644b6f81cbc9e40460fe7eea3c7925

posted @ 2022-05-10 22:49  九天揽月丶  阅读(209)  评论(0编辑  收藏  举报