java漫谈笔记-反射

0x01 反射

反射相关函数

java的动态特性可用以下代码体现:

public void execute(String className, String methodName) throws Exception {
    Class clazz = Class.forName(className);
    clazz.getMethod(methodName).invoke(clazz.newInstance());
}

包括了反射⾥相关的重要⽅法:

  • 获取类的⽅法: forName
  • 实例化类对象的⽅法: newInstance
  • 获取函数的⽅法: getMethod
  • 执⾏函数的⽅法: invoke

forName方法

forName 不是获取“类”的唯⼀途径,通常来说我们有如下三种⽅式获取⼀个“类”,也就是 java.lang.Class 对象:

  • obj.getClass() 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过obj.getClass() 来获取它的类
  • Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接拿它的 class 属性即可。这个⽅法其实不属于反射。
  • Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取

forName有两个函数重载:

  • Class<?> forName(String name)
  • Class<?> forName(String name, **boolean** initialize, ClassLoader loader)
Class.forName(className)
// 等于
Class.forName(className, true, currentLoader)

初始化的理解

初始化顺序:

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());
    }
}

⾸先调⽤的是 static {} ,其次是 {} ,最后是构造函数。

static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯,
但在当前构造函数内容的前⾯。

所以说,forName 中的 initialize=true 其实就是告诉Java虚拟机是否执⾏”类初始化“。

Runtime类的反射

正常情况中需要import引入其他类才能使用,而forName就可以加载任意类;

获得类之后就可以通过反射获取这个类的属性和方法,也可以实例化这个类并调用方法;

class.newInstance() 的作用就是调用这个类的无参构造函数;个人理解其实就是类的实例化;

有时候
在写漏洞利用方法的时候,会发现使用 newInstance 总是不成功,这时候原因可能是:

  1. 你使用的类没有无参构造函数
  2. 你使用的类构造函数是私有的
Class clazz = Class.forName("java.lang.Runtime"); 
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

java.lang.Runtime就是这样,Runtime 类的构造方法是私有的。【设计模式:“单例模式”。(有时候工厂模式也会写成类似)

只有类初始化的时候会执行一次构造函数,后面只能通过 getInstance 获取这个对象;
Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime 对
象。

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");

getMethod 和 invoke 方法

getMethod 的作用是通过反射获取一个类的某个特定的公有方法

我们知道java中支持类的重载,所以再调用getMethod时需要合适的参数类型列表

最简单的,也就是第一个,只有一个参数,类型是String,所以我们使用
getMethod("exec", String.class) 来获取 Runtime.exec 方法。

invoke 的作用是执行方法,它的第一个参数是:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类

以上payload分解后就是:

Class clazz = Class.forName("java.lang.Runtime"); 
Method execMethod = clazz.getMethod("exec", String.class); 
Method getRuntimeMethod = clazz.getMethod("getRuntime"); 
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");

getConstructor

和getMethod对照,getConstructor接受的参数是构造函数列表类型;支持重载;

常用的另一种执行命令的方式ProcessBuilder,使用反射来获取其构造函数,之后调用start()来执行命令;

Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

List构造函数

ProcessBuilder有两个构造函数:

  • public ProcessBuilder(List command)
  • public ProcessBuilder(String... command)

例子中用到的是第一个构造函数,所以传入了List.class;但是例子中用到了java的强制类型转换,漏洞利用的时候是没有这种语法的,因此需要用到反射;

Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance( Arrays.asList("calc.exe")));

通过 getMethod("start") 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是ProcessBuilder Object了。

String数组构造函数

Java里的可变长参数(varargs);Java也支持可变长参数,就是当你定义函数的时候不确定参数数量的时候,可以使用 ... 这样的语法来表示“这个函数的参数个数是可变的”。

对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价的(也就不能重载):

public void hello(String[] names) {} 
public void hello(String...names) {}

那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。

在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给
ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:

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

getDeclared系列反射

如果一个方法或构造方法是私有方法,则使用getDeclared,与常规getMethod、getConstructor 区别是:

  • getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
  • getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了

getDeclaredMethod 的具体用法和 getMethod 类似, getDeclaredConstructor 的具体用法和getConstructor 类似

举个例子,前文我们说过Runtime这个类的构造函数是私有的,我们需要用 Runtime.getRuntime() 来
获取对象。其实现在我们也可以直接用 getDeclaredConstructor 来获取这个私有的构造方法来实例
化对象,进而执行命令:

Class clazz = Class.forName("java.lang.Runtime"); 
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true); 
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");

可见,这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用
setAccessible 修改它的作用域,否则仍然不能调用。

posted @ 2021-01-08 13:09  Yg~  阅读(178)  评论(0)    收藏  举报