反射

开始学习java安全,反射肯定是避不开的,虽然很早之前写过一点记录,但还是再学一遍

Java反射是啥

Java反射就是说,对于任意的一个类,我们都可以通过反射获取这个类中所有的属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制

通过Class类对象来获取StudentTeacher类中的成员变量和成员方法,而不是直接通过创建StudentTeacher类对象,这就叫反射

利用反射创建类对象

获取了Class对象,现在可以通过反射来生成实例化对象,一般我们使用Class对象的newInstance()方法来进行创建类对象。

使用的方式也特别简单,只需要通过forname方法获取到的class对象中进行newInstance方法创建即可。

1
2
Class c = Class.forName("com.reflect.MethodTest"); // 创建Class对象
Object m1 = c.newInstance(); // 创建类对象

利用反射机制创建类并执行方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ReflectTest {
    public void reflectMethod() {
        System.out.println("反射测试成功!!!");
    }
    public static void main(String[] args) {
        try {
            Class c = Class.forName("com.reflect.ReflectTest"); // 创建Class对象
            Object m = c.newInstance(); // 创建类实例对象
            Method method = c.getMethod("reflectMethod"); // 获取reflectMethod方法
            method.invoke(m); // 调用类实例对象方法
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在正常情况下,除了系统类,如果我们想拿到一个类,需要先导入才能使用。而使用forName就不需要,这样对于攻击者来说就十分有利,我们可以加载任意类

同时,forName()还可以获取内部类

我们通过forName获得类之后用newInstance()来调用这个类的无参构造函数,但有时这个方法会失败,原因可能是:

1
2
你使用的类没有无参构造函数
你使用的类构造函数是私有的,而我们并没有权限去调用构造函数

常见的情况就是 java.lang.Runtime ,这个类在我们构造命令执行Payload的时候很常见,但我们不能直接这样来执行命令:

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

因为Runtime类的构造方法是私有的。
但还是有方法获取到这个类的,这涉及到单例模式。

比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连接,就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取:

1
2
3
4
5
6
7
8
public class TrainDB {
private static TrainDB instance = new TrainDB();
public static TrainDB getInstance() {
return instance;
}
    private TrainDB() {
// 建立连接的代码...
}

之后获取这个类的方法为getInstance。

Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime对象。将上述Payload进行修改即可正常执行命令了:

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

Runtime.exec有6个重载,第一个重载,它只有一个参数,类型是String,所以我们使用 getMethod(“exec”, String.class) 来获取 Runtime.exec 方法。
invoke 的作用是执行方法,它的第一个参数是:

1.如果这个方法是一个普通方法,那么第一个参数是类对象

2.如果这个方法是一个静态方法,那么第一个参数是类

这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]…) ,其实在反射里就是 method.invoke([1], [2], [3], [4]…) 。

所以我们将上述命令执行的Payload分解一下就是:

1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");//获取getRuntime方法
Object runtime = getRuntimeMethod.invoke(clazz);//执行getRuntime方法获取Runtime对象
execMethod.invoke(runtime, "calc.exe");

解决两个问题

1.如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?

2.如果一个方法或构造方法是私有方法,我们是否能执行它呢?

那么我们可以尝试获取构造器来实例化类

第一个问题

这里要引入新的反射方法 getConstructor

和 getMethod 类似,getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。

获取到构造函数后,我们使用 newInstance 来执行。

ProcessBuilder有两个构造函数:

1
2
1.public ProcessBuilder(List<String> command)
2.public ProcessBuilder(String... command)

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

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

上面用到了第一个形式的构造函数,所以我在 getConstructor 的时候传入的是 List.class 。
但是,我们看到,前面这个Payload用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表 达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。

其实用的就是前面讲过的知识:

1
2
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了。
那么,如果我们要使用 public ProcessBuilder(String… command) 这个构造函数,需要怎样用反 射执行呢?

这又涉及到Java里的可变长参数了。正如其他语言一样,Java也支持可变长参数,就是当你 定义函数的时候不确定参数数量的时候,可以使用 … 这样的语法来表示“这个函数的参数个数是可变的”。

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

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

那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。
所以,我们将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二种构造函数:

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class);

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

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

第二个问题
这就涉及到 getDeclared 系列的反射了,与普通的 getMethod 、 getConstructor 区别是:

1.getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法

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

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

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