Java安全学习
只开新坑不填旧坑这一块
反射
反射是Java中很重要的一个特性,我们可以使用反射来动态获取一个对象的所有信息。
所以,反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。 ————廖雪峰的官方网站
我们都知道Java作为面向对象语言中最具代表性的一个,在Java中一切皆对象。而每个对象都对应着用来实例化它的类。
如果不用反射或类似的运行时发现机制,程序员必须在编译期/代码中显式绑定类型与实例化逻辑(写 new、写工厂、写注册代码),因此替换实现或动态加载插件时需要改大量代码或依赖构建时/配置时的额外步骤;反射把“选择实现/方法”的时机从编译期延后到运行期,从而降低了代码耦合和样板,但会以运行时安全性、性能和可维护性为代价。。
Java中的注解就是使用反射来实现的,通过反射来获取类并遍历各个类中的方法,从而找到所有想要的方法并注入。以Spring为例,当你在 Spring 配置文件中或者使用注解 @Autowired 时,你只需要告诉 Spring:“我需要一个 UserService 的对象”。Spring 容器在启动时,会读取你的配置,然后通过反射机制,动态地找到 UserService 这个类,调用它的构造函数创建一个实例,然后把它注入到你需要的地方。你并没有写 new UserService() 这行代码,是 Spring 通过反射帮你做了。
如对象序列化 / 反序列化(将对象转为 JSON 或 XML)、ORM(对象关系映射)框架,都需要反射来工作。
举例:JSON 序列化库 (如 Jackson, Gson)
当你调用 new Gson().toJson(myObject) 时,Gson 库并不知道 myObject具体是什么类,有哪些字段。它会使用反射机制,遍历 myObject对象的所有字段(包括 private的),获取它们的名称和值,然后将其组织成 JSON 字符串。
总而言之,有了反射机制,在调用某个对象的方法或访问其字段时,都不再需要知道这个对象对应的类的全部信息,只需要拿到一个实例,就可以尽情地使用它。
Java 的反射 API 提供了一系列的类和接口来操作 Class 对象。主要的类包括:
java.lang.Class:表示类的对象。提供了方法来获取类的字段、方法、构造函数等。java.lang.reflect.Field:表示类的字段(属性)。提供了访问和修改字段的能力。java.lang.reflect.Method:表示类的方法。提供了调用方法的能力。java.lang.reflect.Constructor:表示类的构造函数。提供了创建对象的能力。
一般来说,反射的工作流程是:获取Class对象=>获取成员信息(通过Class获取字段、方法、构造函数等)=>操作成员(读取或者修改值、调用方法、创建对象等)
获取对象
一般使用**Class.forName()来获取对象,使用Class.forName()**获取对象非常简单,我们只需要知道一个类的类名,就可以获取到这个类的对象。
|
|
我们现在知道了有反射,同时知道有个Runtime类可以执行命令,那两者结合,通过这种动态调用的形式就可以执行命令。
Java中有三种代码块:静态初始化块(static block)、实例初始化块(instance initializer block) 和 构造方法(constructor)
|
|
- 静态初始化块(
static {})在类被加载到 JVM 时执行,仅执行一次,只能访问静态成员,同时在任何对象创建之前执行。 - 实例初始化块(
{})每次创建对象时,在构造方法之前执行。 - 构造方法(
public ClassName() {})每次创建对象时,在实例初始化块之后执行。
可以看到静态初始化块中的代码最先执行,构造方法最后执行。而**Class.forName()**就是用来控制静态初始化块的,所以它的优先级非常高。可以往进塞恶意代码:
|
|
创建对象
就是使用getDeclaredConstructor()来创建实例:
|
|
访问字段
可以使用这些方法来访问字段:
clazz.getField(String name):获取公有字段(包括继承的)
clazz.getDeclaredField(String name):获取本类中声明的字段(包括私有字段)
field.setAccessible(true):允许访问私有字段
field.get(Object obj):获取字段值
field.set(Object obj, Object value):设置字段值
代码:
|
|
调用方法
调用方法和访问字段的操作差不多,具体流程为:
获取
Class=>获取目标
Method对象(公有用getMethod,私有用getDeclaredMethod)=>设置可访问(私有方法要
setAccessible(true))=>使用
invoke()调用方法
可以使用这些来获取:
clazz.getMethod(String name, Class… parameterTypes):获取 公有方法(包括继承的)(注意,这里第一个参数是方法名,后面的参数表示的是这个方法传入参数的类型,比如参数是String就是String.class)
clazz.getDeclaredMethod(String name, Class… parameterTypes):获取 本类声明的方法(包括私有方法)
method.setAccessible(true):允许访问私有方法
method.invoke(Object obj, Object… args):调用方法,第一个参数是对象,后面是传入方法的参数
代码例子:
|
|
如果类中存在静态方法,那我们就不需要再实例化对象了,可以直接操作类
就像这样:
|
|
如果方法是静态方法,invoke() 的第一个参数可以传 null,毕竟这个时候我们也没有具体的对象。
现在让我们来弹个计算器
|
|
其中,clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");
我们可以把它看作是一个嵌套的方法调用:
A.invoke(B, "calc.exe"),其中:
A是clazz.getMethod("exec", String.class)的结果。B是clazz.getMethod("getRuntime").invoke(clazz)的结果。
根据规则,JVM 要执行 A.invoke(...),必须先计算出 A 和 B 的值。
- 计算 A 的值: 执行
clazz.getMethod("exec", String.class)。- 这本身又是一个方法调用 (
getMethod),JVM 会先计算它的参数("exec"和String.class),然后执行getMethod,返回一个Method对象(代表exec方法)。现在A的值确定了。
- 这本身又是一个方法调用 (
- 计算 B 的值: 执行
clazz.getMethod("getRuntime").invoke(clazz)。- 这是一个链式调用,我们可以把它拆成
C.invoke(clazz),其中C是clazz.getMethod("getRuntime")的结果。 - 根据规则,要执行
C.invoke(...),必须先计算C的值。 - 计算 C 的值: 执行
clazz.getMethod("getRuntime"),返回一个Method对象(代表getRuntime方法)。现在C的值确定了。 - 执行
C.invoke(clazz): 现在参数都齐了,可以执行invoke方法了。这会调用Runtime.getRuntime(),返回一个Runtime实例。现在B的值也确定了。
- 这是一个链式调用,我们可以把它拆成
- 执行
A.invoke(B, "calc.exe"):- 现在,
invoke方法的所有参数(B和"calc.exe")都计算完毕。 - JVM 终于可以执行
A所代表的exec方法的invoke调用了,这会调用runtimeInstance.exec("calc.exe")。
- 现在,
原因是在Java中,方法调用的参数,总是在方法本身执行之前被计算。
所以上面的代码展开是这样的:
|
|
非无参构造方法和私有方法的调用
我们知道,我们想要调用一个类的方法执行函数,一般情况下都要获取到相应的实例对象,然后调用实例对象的方法函数
上面的例子中,由于没有重载构造方法,所以我们可以直接调用默认的无参构造方法来实例化对象;或是使用了单例模式,使我们得以调用静态方法来获取实例对象。
那如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
另外,如果我们想调用的方法是私有方法,要怎么调用呢?
非无参构造方法
要解决这个问题需要使用getConstructors()方法,这里做一下补充:
java.lang.reflect
java.lang.reflect是 Java 反射机制的核心包,提供了操作类及其成员(字段、方法、构造函数等)的类和接口。通过这些 API,开发者可以在运行时动态地查询和修改类的结构。1.
Class类
功能:表示类的对象,提供了获取类信息的方法,如字段、方法、构造函数等。
主要方法
:
getFields():获取所有公共字段。getDeclaredFields():获取所有声明的字段,包括私有字段。getMethods():获取所有公共方法。getDeclaredMethods():获取所有声明的方法,包括私有方法。getConstructors():获取所有公共构造函数。getDeclaredConstructors():获取所有声明的构造函数,包括私有构造函数。getSuperclass():获取类的父类。getInterfaces():获取类实现的所有接口。2.
Field类
功能:表示类的字段(属性),提供了访问和修改字段值的方法。
主要方法
:
get(Object obj):获取指定对象的字段值。set(Object obj, Object value):设置指定对象的字段值。getType():获取字段的数据类型。getModifiers():获取字段的修饰符(如 public、private)。3.
Method类
功能:表示类的方法,提供了调用方法的能力。
主要方法
:
invoke(Object obj, Object... args):调用指定对象的方法。getReturnType():获取方法的返回类型。getParameterTypes():获取方法的参数类型。getModifiers():获取方法的修饰符(如 public、private)。4.
Constructor类
功能:表示类的构造函数,提供了创建对象的能力。
主要方法
:
newInstance(Object... initargs):创建一个新实例,使用指定的构造函数参数。getParameterTypes():获取构造函数的参数类型。getModifiers():获取构造函数的修饰符(如 public、private)。
getConstructor 接收的参数是构造函数列表类型,因为构造函数支持重载,所以只能通过参数列表来唯一确定一个构造函数。
用ProcessBuilder执行命令来举例:
ProcessBuilder有两个构造函数:
- public ProcessBuilder(List
command) - public ProcessBuilder(String… command)
我们只关注使用反射的方式来获取构造方法:
调用第一个构造函数:
|
|
调用第二个构造函数:
|
|
想通过第二种构造函数实现RCE,这里就需要用到可变长参数了,用...表示参数可变,而由于编译时可变长参数其实会被编译成数组,所以String[]和String…其实没有什么差别
私有方法调用
私有方法只需要setAccessible(true)就行:
|
|
不过这个在Java9及以上版本会报错