Java安全基础第一篇之反射

[TOC]


Java反射定义

Java反射这里指的是我们可以于运行时加载、探知和使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或调用其methods

Java反射相关的类如下:

类名 用途
Class类 代表类的实体,在运行的Java应用程序中表示类和接口
Field类 代表类的成员变量(成员变量也称为类的属性)
Method类 代表类的方法
Constructor类 代表类的构造方法

其中每个类的方法就不详细介绍,可以参考Java高级特性——反射

反射实例

获取class对象

Java反射的本质是获取并使用Class,通常我们有如下4种方式获取一个类的Class对象:

  • 对象.getClass();
  • .class;
  • Class.forName(path);
  • ClassLoader.getSystemClassLoader().loadClass(path);

反射调用内部类的时候需要使用$来代替.,如com.sf.isic类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成:com.sf.isic$Hello

下面以一个实例先搞清楚反射的原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package com.sf.isic;

import java.lang.reflect.*;

public class ReflectClass {

public static void main(String[] args) {
try {

//Class获取类的方法一:通过类的实例对象加载;
User testObject = new User("zhangshan", 19);
Class Method1Class = testObject.getClass();

//Class获取类的方法二:类的.class(最安全/性能最好)属性;有点类似python的getattr()。java中每个类型都有class 属性.
Class Method2Class = User.class;

//Class对象的获取方法三:运用Class.forName(String className)动态加载类,className需要是类的全限定名(最常用).
//这种方法也最容易理解,通过类名(jar包中的完整namespace)就可以调用其中的方法,也最符合我们需要的使用场景.
//j2eeScan burp 插件就使用了这种反射机制。
String path = "com.sf.isic.User";
Class Method3Class = Class.forName(path);

//class对象获取方法四:通过ClassLoader对象的loadClass()方法
Class Method4class = ClassLoader.getSystemClassLoader().loadClass(path);

/* 对于一个任意的可以访问到的类,我们都能够通过下面这些方法来知道它的所有的方法和属性;
* 知道了它的方法和属性,就可以调用这些方法和属性。
*/
Method[] methods = Method3Class.getMethods();
//Field[] fields = Method3Class.getFields();
//Method[] methods = Method2Class.getMethods();
//Method[] methods = Method1Class.getMethods();

// 使用默认无参构造函数,直接调用newInstance进行实例化对象
User user = (User) Method3Class.newInstance();
// 获取getName方法
Method methodGetName = Method3Class.getMethod("getName");
// 用获取到的方法,使用invoke进行调用,参数依次是对象、参数
Object result = methodGetName.invoke(user);//user.getName();
//Object result = methodGetName.invoke(new User());
//new关键字能调用任何构造方法,newInstance()只能调用无参构造方法。但反射的场景中是不应该有机会使用new关键词的。
System.out.println("x" + result);

// 使用带参数的构造方法时,需要先从类中加载构造方法
Constructor constructorMethod = Method3Class.getConstructor(String.class, Integer.class);
// user1 调用了自定义的构造方法,并且传入了参数进行实例化
User user1 = (User) constructorMethod.newInstance("eason", 88);
// 加载setAge方法并使用invoke进行调用
Method methodSetAge = Method3Class.getMethod("setAge", Integer.class);
Object setAgeResult = methodSetAge.invoke(user1, 999);//第一个参数是类的对象。第二参数是函数的参数
//user1.setAge(0000);
System.out.println(user1.getName() + " is " + user1.getAge() + " years old!");

Method methodPrint = Method1Class.getMethod("print", String.class, Integer.class);
Object print = methodPrint.invoke(user1, "eason", 99);
} catch (Exception e1) {
e1.printStackTrace();
}
}
}

class User {
private Integer age;
private String name;

public User() {
this.name = "default name";
}

public User(String name, Integer age) { //构造函数,初始化时执行
this.age = age;
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public void print(String name, Integer age) {
System.out.println(name + " is " + age + " years old!");
}
}

反射与安全

这部分与上面有一些重复,但是更加细节与具体。

在安全方面,反射更多的用处是为了利用其灵活性来构造payload或绕过rasp,接下来以反射调用执行系统命令做更详细的介绍。

不反射调用exec

java.lang.Runtime因为有一个exec方法可以执行本地命令,所以在很多的payload中我们都能看到反射调用Runtime类来执行本地的系统命令,在不采用反射时,一句话就能实现命令执行的效果,如下:

1
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("ipconfig").getInputStream(), "UTF-8"));

反射调用exec

示例代码

先给出反射调用exec的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.sf.isic;

import org.apache.commons.io.IOUtils;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ReflectToExec{
public static void main(String[] args) throws Exception {
// 不使用反射
// System.out.println(IOUtils.toString(Runtime.getRuntime().exec("ipconfig").getInputStream(), "UTF-8"));

// 使用反射
// 加载类
Class runtimeExecClass = Class.forName("java.lang.Runtime");
// 使用getDeclaredConstructor,拿到构造函数
Constructor constructor = runtimeExecClass.getDeclaredConstructor();
// 修改访问权限,因为源码中Runtime类的构造方法修饰符为private
constructor.setAccessible(true);
// 使用newInstance实例化,相当于Runtime rt = new Runtime();
Object runtimeExecInstance = constructor.newInstance();
/* 遍历当前对象拥有的方法
Method[] methods = runtimeExecClass.getMethods();
for (Method method : methods){
System.out.println(method);
}*/
// 加载exec方法
Method runtimeExecMethod = runtimeExecClass.getMethod("exec", String.class);

// 用invoke去调用exec方法
Process process = (Process) runtimeExecMethod.invoke(runtimeExecInstance,"ipconfig");
// 获取命令执行并打印
InputStream in = process.getInputStream();
System.out.println(IOUtils.toString(in, "utf-8"));
}
}

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

  1. 获取Runtime类对象Class.forName("java.lang.Runtime")
  2. 使用对象获取Runtime类的无参数构造方法getDeclaredConstructor(),随后通过newInstance()进行实例化
  3. getMethod()获取exec方法。
  4. invoke()调用exec()方法。

下面分为几个步骤详细讲解一下:

反射创建Runtime类实例

Java中任何一个类都有构造方法,不显示声明的情况下编译过程也会自动生成一个无参构造函数。Runtime类中显示声明了构造方法,如下

image-20200508182215129

可以看到修饰符为private,同时注释说明 Don't let anyone else instantiate this class ,因此在实际使用中也就不能使用 new Runtime() 进行新建一个实例,而是使用 getRuntime() ,在示例中我们借助了反射机制,修改了方法访问权限从而间接的创建出了Runtime对象。

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

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

反射获取Runtime类方法

在上面的示例中我们获取exec方法使用的是getMethod("exec", String.class); 除此之外还可以使用 getDeclaredMethod("方法名",参数1,参数2);getMethodgetDeclaredMethod都能够获取到类成员方法,区别在于getMethod只能获取到当前类和父类的所有有权限的方法(如:public),而getDeclaredMethod能获取到当前类的所有成员方法(不包含父类)。

在一些场景下如果不知道这个类都有哪些方法则可以通过 getDeclaredMethods() 获取一个当前类的所有方法列表。

反射调用Runtime类方法

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

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

method.invoke的第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值则需要传null,因为在java中调用static方法是不需要有类实例的。

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

反射操作成员变量

这里的操作在上面的代码没有体现,单独拿出来学习。在使用反射时,可以做到无视权限修饰符来修改成员变量的值。

获取当前类的所有成员变量:Field fields = class.getDeclaredFields();

获取当前类指定的成员变量:Field field = class.getDeclaredField("变量名");

这里 getFieldgetDeclaredField的区别同getMethodgetDeclaredMethod

获取和修改成员变量值分别为:

1
2
3
Object obj = field.get(类实例对象);

Object obj = field.set(类实例对象, 修改后的值);

同理,当我们没有修改的成员变量权限时也可以使用: setAccessible(true)的方式修改为访问成员变量访问权限。

如下使用该方法来修改原类中 privatefinal 修饰符的成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.sf.isic;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class ReflectField {
public static void main(String[] args) throws Exception {
Class testField = ToBeReflect.class;
Constructor constructor = testField.getDeclaredConstructor();
ToBeReflect testField1 = (ToBeReflect) constructor.newInstance();
// 反射获取成员变量
Field numA = testField.getDeclaredField("numA");
Field numB = testField.getDeclaredField("numB");
// 更改权限,不然下面的set会报错
numA.setAccessible(true);
numB.setAccessible(true);
// 使用field.set(obj, value) 来修改值
numA.set(testField1, 20);
numB.set(testField1, 20);

// true
System.out.println(numA.get(testField1)==numB.get(testField1));
}
}

class ToBeReflect{
final Integer numA = 100;
private Integer numB = 200;
public ToBeReflect(){ }
}

反射总结

通过例子可以看到利用反射机制我们可以轻松的实现Java类的动态调用。对于常见的Java命令执行漏洞,最终的利用payload都会使用到反射和反序列化,因此搞清楚反射原理对于理解漏洞还是很有必要的。

参考链接

Java反序列化漏洞学习实践二:Java的反射机制(Java Reflection) -bit4woo

Java反射机制

Java高级特性——反射

下一篇 Java安全基础之 (反)序列化

​ 2020/05/08