总结学习P神星球上发的文章。
GitHub - phith0n/JavaThings: Share Things Related to Java - Java安全漫谈笔记相关内容
反射机制是什么
Oracle 官方对反射的解释是:
1 | Reflection is commonly used by programs which require the ability to examine or |
Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
1、Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
2、Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。
- 我们编译时知道类或对象的具体信息,此时直接对类和对象进行操作即可,无需使用反射(reflection)
- 如果编译不知道类或对象的具体信息,此时应该如何做呢?这时就要用到 反射 来实现。比如类的名称放在XML文件中,属性和属性值放在XML文件中,需要在运行时读取XML文件,动态获取类的信息
Java虽不像PHP那么灵活, 但其提供的“反射”功能,也是可以提供⼀些动态特性。这样⼀段代码,在你不知道传⼊的参数值 的时候,你是不知道他的作⽤是什么的
1 | public void execute(String className, String methodName) throws Exception { |
获取类的⽅法: forName
实例化类对象的⽅法: newInstance
获取函数的⽅法: getMethod
执⾏函数的⽅法: invoke
正射与反射的对比
有反射就有正射,有对比可以更加直观了解反射。
正射
我们在编写代码时,当需要使用到某一个类的时候,都会先了解这个类是做什么的。然后实例化这个类,接着用实例化好的对象进行操作,这就是正射。
1 | Student student = new Student(); |
反射
反射就是,一开始并不知道我们要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。
1 | Class clazz = Class.forName("reflection.Student"); |
- 第一段代码在未运行前就已经知道了要运行的类是
Student
; - 第二段代码则是到整个程序运行的时候,从字符串
reflection.Student
,才知道要操作的类是Student
。
Class 对象理解
要理解Class对象,我们先来了解一下RTTI吧。 RTTI(Run-Time Type Identification)运行时类型识别,其作用是在运行时识别一个对象的类型和类的信息。
Java是如何让我们在运行时识别对象和类的信息的?主要有两种方式: 一种是传统的RRTI,它假定我们在编译期已知道了所有类型。 另一种是反射机制,它允许我们在运行时发现和使用类的信息。
每个类都有一个Class对象,每当编译一个新类就产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。比如创建一个Student类,那么,JVM就会创建一个Student对应Class类的Class对象,该Class对象保存了Student类相关的类型信息。
Class类的对象作用是运行时提供或获得某个对象的类型信息
反射的基本使用
Java 异常 (Try…Catch) 语句 (w3schools.cn)
获取 Class 类对象
获取反射中的Class对象有三种方法。(了Java安全⾥各种和反射有关的Payload常用到)
- obj.getClass() 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过 obj.getClass() 来获取它的类
- Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接 拿它的 class 属性即可。这个⽅法其实不属于反射。
- Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取
1 | package xz; |
对比
1 | package com.example.xz; |
forName有两个函数重载:
1 | Class forName(String name) |
第⼀个就是我们最常⻅的获取class的⽅式,其实可以理解为第⼆种⽅式的⼀个封装:
1 | Class.forName(className) |
默认情况下, forName 的第⼀个参数是类名;第⼆个参数表示是否初始化;第三个参数就 是 ClassLoader 。
ClassLoader 是什么呢?它就是⼀个“加载器”,告诉Java虚拟机如何加载这个类(漏洞利用⽅法)。Java默认的 ClassLoader 就是根据类名来加载类, 这个类名是类完整路径,如 java.lang.Runtime 。
这段代码使用反射机制动态地获取和分析了test
类的信息,包括类的方法、名称和修饰符。在遍历方法时,如果找到了名为”int2string”的方法,还获取了该方法的参数类型。
1 | package Test; |
使⽤功能”.class”来创建Class对象的引⽤时,不会⾃动初始化该Class对象,使⽤forName()会⾃动初始化该Class对象。
其实在 forName 的时候,构造函数并不会执行,即使我们设置initialize=true 。
可以将这个“初始化”理解为类的初始化。我们先来看看如下这个类:
1 | package Test; |
运⾏⼀下就知道了,⾸先调⽤的是 static {}
,其次是 {}
,最后是构造函数。 其中, static {}
就是在“类初始化”的时候调⽤的,⽽ {}
中的代码会放在构造函数的 super()
后⾯, 但在当前构造函数内容的前⾯。 所以说, forName
中的 initialize=true
其实就是告诉Java虚拟机是否执⾏”类初始化“。
forName()方法源代码分析
1 | public static Class<?> forName(String className) |
forName
方法是一个静态方法,它接受一个字符串参数className
,表示要加载的类的全限定名。- 首先,通过调用
Reflection.getCallerClass()
获取调用者的类。Reflection
是Java中用于提供对反射操作的支持的类。 - 接下来,通过调用
ClassLoader.getClassLoader(caller)
获取调用者的类加载器。这一步是为了获取加载调用者类的类加载器。 - 最后,调用
forName0
方法,该方法是一个本地方法(native method),实际的实现在本地代码中。这个方法使用传递进来的类名、是否需要初始化、类加载器以及调用者的类来加载指定的类。
那么,假设我们有如下函数,其中函数的参数name可控:
1 | package Test; |
我们就可以编写⼀个恶意类,将恶意代码放置在 static {} 中,从⽽执⾏:
RunCalculator.java
1 | package Test; |
这个恶意类如何带⼊⽬标机器中,可能就涉及到ClassLoader的⼀些利⽤⽅法了。
反射创造对象
Java.lang.Class.newInstance() 方法 (w3schools.cn)
通过反射创建类对象主要有两种方式:
java.lang.Class.newInstance() 创建由这个 Class 对象表示的类的新实例。 该类被实例化为一个带有空参数列表的新表达式。 如果尚未初始化该类,则将其初始化。
1 | package xz; |
在正常情况下,除了系统类,如果我们想拿到一个类,需要先 import 才能使用。而使用forName就不 需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。
编译java文件的时候会发现一个java文件可以生成的多个class文件,而且有的还含有“$”符号,这个符号代表的是内部类。
Java的普通类 C1
中支持编写内部类 C2
,而在编译的时候,会生成两个文件: C1.class
和 C1$C2.class
,我们可以把他们看作两个无关的类,通过 Class.forName("C1$C2")
即可加载这个内部类。
获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方法。
class.newInstance()
的作用就是调用这个类的无参构造函数,这个比较好理解。不过,我们有时候 在写漏洞利用方法的时候,会发现使用 newInstance
总是不成功,这时候原因可能是:
你使用的类没有无参构造函数
你使用的类构造函数是私有的
常见的情况就是
java.lang.Runtime
,这个类在我们构造命令执行Payload的时候很常见,但我们不能直接这样来执行命令:1
2Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
原因是 Runtime
类的构造方法是私有的。
为什么会有类的构造方法是私有的?
其实涉及 到很常见的设计模式:“单例模式”。(有时候工厂模式也会写成类似)
ok,那什么是单列模式呢?
单列模式
java单例模式——详解JAVA单例模式及8种实现方式_单例模式java实现-CSDN博客
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连 接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来 获取:
1 | public class TrainDB { |
这样,只有类初始化的时候会执行一次构造函数,后面只能通过 getInstance
获取这个对象,避免建 立多个数据库连接。
Runtime类就是单例模式,我们只能通过 Runtime.getRuntime()
来获取到 Runtime
对 象。我们将上述Payload进行修改即可正常执行命令了:
1 | package Test; |
这里用到了 getMethod
和 invoke
方法。 getMethod
的作用是通过反射获取一个类的某个特定的公有方法。Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod
的时候,我们需要传给他你需要获取的函数的参数类型列表。 比如这里的 Runtime.exec
方法有6个重载:
Java.lang.Runtime 类 (w3schools.cn)
我们使用最简单的,也就是第一个,它只有一个参数,类型是String,所以我们使用 getMethod("exec", String.class)
来获取 Runtime.exec
方法。
invoke
的作用是执行方法,它的第一个参数是:
- 如果这个方法是一个普通方法,那么第一个参数是类对象
- 如果这个方法是一个静态方法,那么第一个参数是类 这也比较好理解了,我们正常执行方法是
[1].method([2], [3], [4]...)
,其实在反射里就是method.invoke([1], [2], [3], [4]...)
。
上述命令执行的Payload分解一下就是:
1 | Class clazz = Class.forName("java.lang.Runtime"); |
反射获取类的构造器
如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
我们需要用到一个新的反射方法 getConstructor
。 和 getMethod
类似, getConstructor
接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。 获取到构造函数后,我们使用 newInstance
来执行。
使用构造器时需要记住:
1.构造器必须与类同名(如果一个源文件中有多个类,那么构造器必须与公共类同名)
2.每个类可以有一个以上的构造器
3.构造器可以有0个、1个或1个以上的参数
4.构造器没有返回值
5.构造器总是伴随着new操作一起调用
6.不添加任何构造器会默认有空的构造器
1 | package xz; |
代码Constructor<?> one = clazz.getDeclaredConstructor(String.class);
中的参数是String.class
而不是直接String
,是因为Java中的反射API通过.class
来获取类的Class对象。.class
是Java语言用于获取类的Class对象的语法,可以用于获取各种类型的Class对象。
如果要调用的构造函数具有int、char、double等基本数据类型的参数,可以使用相应的包装类的.class
来表示,例如:
- int:
int.class
- char:
char.class
- double:
double.class
通过使用包装类的.class
,可以获取对应基本数据类型的Class对象,并在获取构造函数时进行使用。
main
方法,主要进行了以下操作:
获取类信息:
1
2javaCopy code
Class<?> clazz = Member.class;通过
Member.class
获取Member
类的Class
对象。这是利用 Java 的反射机制的一部分,用于在运行时获取类的信息。获取构造函数信息:
1
2javaCopy code
Constructor<?>[] cons = clazz.getDeclaredConstructors();通过
getDeclaredConstructors()
方法获取Member
类中所有声明的构造函数。这里使用的是getDeclaredConstructors()
,它返回所有访问级别的构造函数,包括private
。输出所有构造函数:
1
2
3javaCopy codefor (Constructor<?> con : cons) {
System.out.println(con);
}通过遍历构造函数数组,输出每个构造函数的信息。这包括构造函数的修饰符、参数类型等。
获取单参构造函数:
1
2javaCopy code
Constructor<?> one = clazz.getDeclaredConstructor(String.class);使用
getDeclaredConstructor()
方法获取单参数为String
类型的构造函数。通过反射创建对象实例:
1
2javaCopy code
Object obj = one.newInstance("我是你爷爷");利用获取到的单参数构造函数
one
,通过newInstance()
方法创建一个Member
类的对象实例,并传入参数 “我是你爷爷”。输出对象信息:
1
2javaCopy code
System.out.println(obj);输出对象的信息。这里会调用
Member
类中的toString()
方法,该方法被重写,返回包含成员变量str
的字符串表示。
综合起来,这段代码的主要目的是演示如何通过反射获取类的构造函数信息,遍历并输出所有构造函数,然后使用特定构造函数创建对象实例,并输出对象信息。在这个例子中,它获取了 Member
类的构造函数信息,创建了一个具有特定参数的对象,并输出了对象的字符串表示。
攻击中的利用ProcessBuilder
比如,我们常用的另一种执行命令的方式ProcessBuilder
,我们使用反射来获取其构造函数,然后调用 start()
来执行命令:
ProcessBuilder (Java SE 11 & JDK 11 ) (runoob.com)
ProcessBuilder
有两个构造函数:
1 | public ProcessBuilder(List command) |
利用漏洞的payload用到了ava里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。
1 | package Test; |
通过 getMethod("start")
获取到start
方法,然后 invoke
执行, invoke
的第一个参数就是 ProcessBuilder Object
了。
如果我们要使用 public ProcessBuilder(String... command)
这个构造函数,需要怎样用反射执行呢?
这又涉及到Java里的可变长参数(varargs)了。正如其他语言一样,Java也支持可变长参数,就是当你 定义函数的时候不确定参数数量的时候,可以使用 … 这样的语法来表示“这个函数的参数个数是可变 的”。 对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价 的(也就不能重载):
1 | public void hello(String[] names) {} |
也由此,如果我们有一个数组,想传给hello函数,只需直接传即可:
1 | String[] names = {"hello", "world"}; |
那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。
所以,我们将字符串数组的类 String[].class
传给 getConstructor
,获取 ProcessBuilder
的第二种构造函数:
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
在调用 newInstance
的时候,因为这个函数本身接收的是一个可变长参数,我们传给 ProcessBuilder
的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
反射获取类的成员变量
如果一个方法或构造方法是私有方法,我们是否能执行它呢?
这就涉及到 getDeclared
系列的反射了,与普通的 getMethod
、 getConstructor
区别是:
getMethod
系列方法获取的是当前类中所有公共方法,包括从父类继承的方法getDeclaredMethod
系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了。
getDeclaredMethod
的具体用法和 getMethod
类似, getDeclaredConstructor
的具体用法和 getConstructor
类似
Java.lang.Class.getMethod() 方法 (w3schools.cn)
描述
java.lang.Class.getMethod() 返回一个 Method 对象,该对象反映了该 Class 对象所代表的类或接口的指定公共成员方法。 name 参数是一个字符串,指定所需方法的简单名称。
parameterTypes 参数是一个 Class 对象数组,它们按照声明的顺序标识方法的形式参数类型。 如果 parameterTypes 为 null,则将其视为空数组。.
声明
以下是 java.lang.Class.getMethod() 方法的声明。
1 | public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException |
参数
- name − 这是方法的名称。
- parameterTypes − 这是参数列表。
返回值
此方法返回与指定名称和参数类型匹配的 Method 对象。
攻击利用getDeclaredConstructor
前文我们说过Runtime这个类的构造函数是私有的,我们需要用 Runtime.getRuntime() 来 获取对象。其实现在我们也可以直接用 getDeclaredConstructor 来获取这个私有的构造方法来实例 化对象,进而执行命令:
1 | package Test; |
可见,这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。
注:JDK17会抛出异常。据说换JDK18可以