当前位置: 首页 > news >正文

【Java安全】反射基础

文章目录

    • 反射
    • 获取类
    • 获取类方法
    • 获取成员变量
    • 获取构造函数
    • 创建对象
    • 单例模式
    • 两个问题
      • 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
      • 如果一个方法或构造方法是私有方法,我们是否能执行它呢?

反射

Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为 Java 语言的反射机制。

对象可以通过反射获取他的类,类可以通过反射拿到所有 方法和字段(包括私有)

反射主要重点是注意以下几个类:

  • java.lang.Class:表示类的对象。提供了方法来获取类的字段、方法、构造函数等。
  • java.lang.reflect.Field:表示类的字段(属性)。提供了访问和修改字段的能力。
  • java.lang.reflect.Method:表示类的方法。提供了调用方法的能力。
  • java.lang.reflect.Constructor:表示类的构造函数。提供了创建对象的能力。

反射中常用的几个重要方法:

  • 获取类的⽅法:forName
  • 实例化类对象的⽅法: newInstance
  • 获取函数的⽅法: getMethod
  • 执⾏函数的⽅法: invoke
  • 限制突破方法:setAccessible

获取类

通常有三种方法可以获取类, 也就是 java.lang.Class 对象

一. class.forName()

知道某个类的全名, 想要获取这个类, 可以直接通过其全名来拿到这个类

package Reflect;public class demo1 {public static void main(String[] args) throws Exception {Class<?> clazz1 = Class.forName("java.lang.Runtime");System.out.println(clazz1);}
}

在这里插入图片描述

二. obj.getClass()

如果上下文中存在某个类的实例 obj, 可以通过obj.getClass()来获取它的类

package Reflect;public class demo1 {public static void main(String[] args) throws Exception {Integer Int = 1;Class<?> clazz2 = Int.getClass();System.out.println(clazz2);demo1 demo1 = new demo1();Class<?> clazz3 = demo1.getClass();System.out.println(clazz3);}
}

在这里插入图片描述

三. 类的.class方法

如果已经加载了某个类, 那么就可以直接通过它的class属性来获取其Class对象
(这个方法不属于反射)

package Reflect;import OOP.Person;public class demo1 {public static void main(String[] args) throws Exception {Class<?> clazz1 = TrainPrint.class;Class<?> clazz2 = String.class;Class<?> clazz3 = Person.class;System.out.println(clazz1);System.out.println(clazz2);System.out.println(clazz3);}
}

在这里插入图片描述

这些动态的执行方法也给程序带来了危险性, 即使上下文中只有一个数字, 也可以通过反射来获取可以执行命令的Runtime类

package Reflect;public class demo1 {public static void main(String[] args) throws Exception {Integer a = 1;Class<?> run=a.getClass().forName("java.lang.Runtime");System.out.println(run);}
}

在这里插入图片描述

forName有两个函数重载:

  • Class forName(String name)
  • Class forName(String name, boolean initialize, ClassLoader loader)

第⼀个就是最常⻅的获取class的⽅式,其实可以理解为第⼆种⽅式的⼀个封装

Class.forName(className)
// 等于
Class.forName(className, true, currentLoader)

默认情况下, forName 的第一个参数是类名;第⼆个参数表示是否初始化;第三个参数就是 ClassLoader(类的加载器)

Java默认的 ClassLoader 就是根据类名来加载类 , 这个类名是类的完整路径

先理解Java的三种代码块:静态初始化块实例初始化块构造方法 , 它们在初始化的时候其执行的先后顺序是依次往后的

  • 静态初始化块(static {})在类被加载到 JVM 时执行,仅执行一次,只能访问静态成员,同时在任何对象创建之前执行。
  • 实例初始化块({})每次创建对象时,在构造方法之前执行。
  • 构造方法(public ClassName() {})每次创建对象时,在实例初始化块之后执行。
package Reflect;public class demo1 {public static void main(String[] args) throws Exception {new test();}
}class test{public test() {System.out.println("构造方法");}{System.out.println("实例初始化块");}static {System.out.println("静态初始化块");}}

在这里插入图片描述

forName()第⼆个参数 initialize 初始化指的是 是否执行”类初始化“, 它只会执行的是类里面static{}静态块里面的内容

如果可以写入一个恶意类, 那么在它的static{}里面写入恶意代码, 就可以使用forName获取这个类的时候直接执行恶意代码

package Reflect;public class demo1 {public static void main(String[] args) throws Exception {Class.forName("Reflect.test");}
}class test{public test() {System.out.println("构造方法");}{System.out.println("实例初始化块");}static {System.out.println("Hack Hack");}}

在这里插入图片描述

获取类方法

  • Method getMethod(String name, 类<?>… parameterTypes) : 返回该类所声明的public方法
  • Method getDeclaredMethod(String name, 类<?>… parameterTypes) : 返回该类所声明的所有方法

第一个参数是传参, 获取方法的名字;

第二个参数是指明方法的参数类型的class对象, 主要是有多个重载函数时区分获得哪一个函数

  • Method[] getMethods() : 获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法
  • Method[] getDeclaredMethods() : 获取该类中的所有方法
package Reflect;import java.lang.reflect.Method;public class demo2 {public static void main(String[] args) throws Exception {Class<?> clazz = Class.forName("Reflect.test2");//获取单个公有方法Method m1=clazz.getMethod("demo1",String.class);//获取单个私有方法Method m2=clazz.getDeclaredMethod("demo2",Integer.class);//获取该类中所有方法Method[] m3=clazz.getDeclaredMethods();//获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法Method[] m4=clazz.getMethods();System.out.println(m1);System.out.println("---------------------------");System.out.println(m2);System.out.println("---------------------------");for (Method m : m3) {System.out.println(m);}System.out.println("---------------------------");for (Method m : m4) {System.out.println(m);}}
}class test2{public void demo1(String a){System.out.println(a);}public int demo1(Integer a){return a;}private void demo2(String a){System.out.println(a);}private int demo2(Integer a){return a;}
}

在这里插入图片描述

获取成员变量

获取成员变量Field位于 java.lang.reflect.Field 包中

  • Field[] getFields() :获取所有 public 修饰的成员变量,包括父类的
  • Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
  • Field getField(String name) 获取指定名称的 public 修饰的成员变量
  • Field getDeclaredField(String name) 获取指定的成员变量

获取构造函数

  • Constructor<?>[] getConstructors() :只返回public构造函数
  • Constructor<?>[] getDeclaredConstructors() :返回所有构造函数
  • Constructor<> getConstructor(类<?>… parameterTypes) : 匹配和参数配型相符的public构造函数
  • Constructor<> getDeclaredConstructor(类<?>… parameterTypes) : 匹配和参数配型相符的构造函数

参数指的是方法参数类型的class对象

创建对象

  • newInstance() : 通过构造方法创建此 Class 对象所表示的类的一个新实例

  • setAccessible(true): 默认为false;true则忽略构造方法访问修饰符的安全检查,主要突破private的限制

  • invoke(obj, … parameterTypes) : 调用函数执行, 它的第一个参数是:

    • 如果这个方法是一个普通方法,那么第一个参数是类对象实例
    • 如果这个方法是一个静态方法,那么第一个参数是类(也可以传**null**)
package Reflect;import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;public class demo3 {public static void main(String[] args) throws Exception {Class<?> clazz = Class.forName("Reflect.test3");//通过构造函数创建对象Constructor<?> cons = clazz.getConstructor(Integer.class);test3 obj = (test3)cons.newInstance(1);System.out.println("----------------");//直接创建对象,但是只能是调用 public 的无参构造方法test3 test = (test3)clazz.newInstance();System.out.println("----------------");//访问公有字段Field name = clazz.getField("name");System.out.println("obj实例name: " + name.get(obj));System.out.println("test实例name: " + name.get(test));name.set(obj,"123");System.out.println("更新后obj实例name: " + name.get(obj));System.out.println("更新后test实例name: " + name.get(test));System.out.println("----------------");//访问私有字段Field age = clazz.getDeclaredField("age");age.setAccessible(true);System.out.println("私有age: " + age.get(obj));age.set(obj,11);System.out.println("更新后私有age:"+age.get(obj));System.out.println("----------------");//调用公有方法Method m1 = clazz.getMethod("add",Integer.class,Integer.class);Integer result =(Integer)  m1.invoke(obj,1,2);System.out.println("调用add方法:"+result);//调用私有方法Method m2 = clazz.getDeclaredMethod("say");m2.setAccessible(true);m2.invoke(obj);}
}class test3{public String name = "xpw";private int age = 1;public test3() {System.out.println("无参构造方法");}public test3(Integer a){System.out.println("有参构造方法");System.out.println(a);}public Integer add(Integer a,Integer b){return a+b;}private void say(){System.out.println("调用了私有方法say");}
}

在这里插入图片描述

单例模式

一个类在整个应用中只有一个实例,并提供一个全局访问点。

一个标准的单例模式通常包括:

  1. 私有构造方法 —— 防止外部创建实例
  2. 私有静态变量 —— 存储唯一实例
  3. 公共静态方法 —— 提供全局访问入口

常用的用来执行命令的Runtime类就是单例模式, 整个 JVM 运行中只有一个 Runtime 实例,负责底层系统命令执行、进程管理等重要功能。

所以通过反射使用Runtime不能去新建一个实例进行命令执行, 会报错, 比如:

package Reflect;import java.lang.reflect.Method;public class demo4 {public static void main(String[] args)throws Exception {Class clazz = Class.forName("java.lang.Runtime");Method rt_exec = clazz.getMethod("exec", String.class);rt_exec.invoke(clazz.newInstance(), "calc.exe");}
}

在这里插入图片描述

看到Runtime的源码可知, 它的构造函数是私有的, 所以无法新建实例

只能通过它的getRuntime()来获得Runtime的实例对象

在这里插入图片描述

也就是说在反射中要执行命令得这样写

package Reflect;import java.lang.reflect.Method;public class demo4 {public static void main(String[] args)throws Exception {
//        Class clazz = Class.forName("java.lang.Runtime");
//        Method rt_exec = clazz.getMethod("exec", String.class);
//        rt_exec.invoke(clazz.newInstance(), "calc.exe");//1.获取Runtime对象Class clazz = Class.forName("java.lang.Runtime");//2.获取gerRuntime()静态方法Method m1 = clazz.getMethod("getRuntime");//3.调用getRuntime静态方法, 获得Runtime实例Object rInstance = m1.invoke(null);//4.获得exec方法Method m2 = clazz.getMethod("exec", String.class);//5.执行exec方法m2.invoke(rInstance, "calc.exe");}
}

在这里插入图片描述

两个问题

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

可以通过反射获取指定的有参构造器,然后使用newInstance 来创建对象即可

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

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

在这里插入图片描述

ProcessBuilder有两个构造函数:

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

上面用到了第一个形式的构造函数,所以我在 getConstructor 的时候传入的是 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了

在这里插入图片描述

它的第二个构造函数public ProcessBuilder(String... command) 的利用需要使用到可变长参数
使用 … 这样的语法来表示“这个函数的参数个数是可变的”

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

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

所以将字符串数组的类String[].class传给 getConstructor , 就可以获取 ProcessBuilder 的第二种构造函数 :

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

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

如果传入的是一维数组会报 参数类型不匹配 的错误, Java的自动拆包, 编译器会自动 **String[]** 拆开当成了多个 **Object** 参数, 也就是会导致传入进去的是一个字符串而不是一个数组

也可以使用 (Object) 强制包装成一个单独参数

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

在这里插入图片描述

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

其实就是利用到 getDeclaredMethod, getDeclaredConstructor等, 它们获取当前类中“声明”的方法(包括私有), 然后再配合 setAccessible(true)允许对private类型的进行修改

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

在这里插入图片描述

但是这样的写法在 Java 9 之后默认会抛出报错:
Unable to make private java.lang.Runtime() accessible

在这里插入图片描述

不过仍然可以在java 9中加 VM 参数:--add-opens java.base/java.lang=ALL-UNNAMED 来临时绕过

在这里插入图片描述

在这里插入图片描述

参考文章

Java安全漫谈 反射篇
https://fushuling.com/index.php/2023/01/30/java%e5%ae%89%e5%85%a8%e7%ac%94%e8%ae%b0/
https://drun1baby.top/2022/05/20/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-02-Java%E5%8F%8D%E5%B0%84%E4%B8%8EURLDNS%E9%93%BE%E5%88%86%E6%9E%90/
http://www.dtcms.com/a/268857.html

相关文章:

  • (倍增)洛谷 P1613 跑路/P4155 国旗计划
  • VBA之Word应用第三章第十一节:Document对象的事件
  • 图像采集卡选型详细指南
  • SAP HANA内存数据库解析:特性、优势与应用场景 | 技术指南
  • OceanBase在.NET开发操作指南
  • 5、Receiving Messages:Message Listener Containers
  • 【Note】《Kafka: The Definitive Guide》第7章 Building Data Pipelines
  • 【牛客刷题】实现返回最大的不大于n的完美数的函数
  • [NOIP][C++]洛谷P1035 [NOIP 2002 普及组] 级数求和
  • 牛客周赛 Round 99 EF
  • 【PTA数据结构 | C语言版】求两个正整数的最大公约数
  • Camera相机人脸识别系列专题分析之十六:人脸特征检测FFD算法之libcvface_api.so数据结构详细注释解析
  • 芯谷科技--高性能双通道音频功率放大器D7050
  • LabVIEW与FPGA超声探伤
  • 单细胞数据格式转换:rds 与 h5ad互转
  • 倒排索引(Inverted Index)深度解析
  • uniapp跳转页面时如何带对象参数
  • 后端微服务基础架构Spring Cloud
  • UI前端与数字孪生融合新领域:智慧旅游的可视化导览系统
  • kong网关基于header分流灰度发布
  • Linux手动安装MySQL(基于CentOS 7 X86)
  • HCI接口协议:主机与控制器通信的标准桥梁(面试深度解析)
  • Ubunt20.04搭建GitLab服务器,并借助cpolar实现公网访问
  • Taro+Vue3实现微信小程序富文本编辑器组件开发指南
  • RoboRefer:面向机器人视觉-语言模型推理的空间参考
  • 数学建模从入门到国奖——备赛规划优秀论文学习方法
  • 在 Windows 系统上配置 [go-zero](https://go-zero.dev) 开发环境教程
  • React-React.memo-props比较机制
  • 基于YOLOv11的车辆检测系统项目教程(Python源码+Flask Web界面+数据集)
  • AI智能体长期记忆系统架构设计与落地实践:从理论到生产部署