获取中...

-

Just a minute...

反射概念

在java之中 万物都是对象 包括“类本身也是一个对象”(属于java.lang.Class类的对象)

反射的本质就是在程序运行期间 通过操控这个Class对象 反向解析和操控它所代表的哪个类里面的内容(构造器,属性,方法)

实例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 假设这是一个系统的底层核心类,防护严密
public class SecretVault {
// 私有属性
private String secretKey = "Default_Key_123";

// 私有构造器(防止外部 new 对象)
private SecretVault() {
System.out.println("[-] 私有构造器被调用,对象已生成。");
}

// 私有方法
private void openVault(String password) {
if ("HACKED".equals(password)) {
System.out.println("[!] 警告:金库已被打开!当前密钥:" + this.secretKey);
} else {
System.out.println("[-] 密码错误。");
}
}
}

代码分析

  • 类本身是公开的
  • secretKey使用private修饰符 意味着这个变量处理类自己的代码 外部无法直接使用SecretValut.secretKey调用(正常路径拿不到 故而使用反射机制)
  • private SecretVault():构造器设置为私有 未来控制对象的创建 把创建对象的控制权捏在自己手里

那么现在我们需要让password等于HACKED 但是由于私有方法的控制 没法直接赋值

构造反射payload

获取Class对象

在java虚拟机JVM之中 写的代码(比如 SecretVault 类)在被加载到内存时 JVM会自动为它创建一个特殊的对象 这个对象的类型就是 java.lang.Class
这个对象包含这个类的所有属性 方法等详细
为了获取这个镜像类 Java提供了三种方法

方法一:动态加载——Class.forName()

1
2
// 假设我们想拿到 java.lang.Runtime 这个类的图纸
Class<?> clazz1 = Class.forName("java.lang.Runtime");

接收一个字符串作为参数 这个字符串必须是类的全限定名——包名+类名
攻击者在外部能传给服务器的通常只有字符串 而Class.forName() 恰好提供了一个将外部可控的字符串 转化为JVM内部执行代码的桥梁

值得注意的是 它会抛出ClassNotFoundException(找不到类异常)这是因为传的是字符串 编译器没法提前帮你检查写错没写错 只能在运行时候报错

方法二:静态获取——类名.class
这是最简单最安全的 是业务开发之中比较常用的方式

1
Class<?> clazz2 = java.lang.Runtime.class;

直接在已知的类名后面硬编码(将数据(如字符串、数字、类名等)直接写在源代码中,而不是通过变量、配置文件、用户输入或参数动态传入)写上.class
不需要处理异常 因为些这个代码的时候这个类不存在 编译器直接就标红报错了

方法三:运行时推断——对象.getClass()
当手里依旧有了一个活的对象实体 想反向查询它的图纸的时候使用

1
2
3
// 假设由于某种原因,你已经得到了一个 runtimeObj 对象
Runtime runtimeObj = Runtime.getRuntime();
Class<?> clazz3 = runtimeObj.getClass();
  • getClass()是java所有对象的(java.lang.Object 类)提供的方法 任何对象都能调用它

不管使用哪一种方法· JVM在内存里面都只会保存一份该类的Class对象

获取并突破构造器

对比两种获取方法

  • getConstructor(...):只能获取类之中被声明为public的构造器 如果使用这个获取private构造器 代码会直接报错并抛出异常
  • getDeclaredConstructor(..,):代理Declared(声明过的)这个词 就像拥有了透视眼 无视任何修饰符只要这个类里面泄露的构造器它都能拿出来
    但是拿到构造器并不等于能够使用它 java的访问控制机制还在起作用 这时候直接去实例化 java还是会拦住你并且抛出IllegalAccessException(非法访问异常)
设置可访问性为真
1
constructor.setAccessible(true);

在底层直接关闭了java语言的访问安全检查

创造实体

newInstance(...):创造实体 等同于写正常代码时候的new关键字

完整payload

将以下代码和SecretVault类java文件放在·同一目录下

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
import java.lang.reflect.Constructor;  
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main {

public static void main(String[] args) {
System.out.println("[*] 开始执行反射攻击 Payload...");

try {
// ==========================================
// 第一步:获取目标类的 Class 对象 (获取图纸)
// ========================================== System.out.println("[*] Step 1: 正在动态加载目标类 SecretVault...");
// 注意:这里假设 SecretVault 和当前类在同一个包(或者没有包名)。
// 如果在实际项目中,需要写完整的包名,例如 "com.system.core.SecretVault" Class<?> targetClass = Class.forName("SecretVault");


// ==========================================
// 第二步:突破私有构造器,强行实例化对象 (强行造物)
// ========================================== System.out.println("[*] Step 2: 寻找并突破私有构造器...");
Constructor<?> constructor = targetClass.getDeclaredConstructor();
// 【核心】关闭安全检查,打破 private 屏障
constructor.setAccessible(true);
// 强行实例化对象
Object vaultInstance = constructor.newInstance();


// ==========================================
// 第三步:篡改私有属性的值 (偷梁换柱)
// ========================================== System.out.println("[*] Step 3: 正在篡改私有变量 secretKey...");
Field secretKeyField = targetClass.getDeclaredField("secretKey");
// 【核心】打破属性的 private 屏障
secretKeyField.setAccessible(true);
// 将 vaultInstance 对象中的 secretKey 强行修改为我们的黑客标识
secretKeyField.set(vaultInstance, "CTF_PWNED_BY_REFLECTION");


// ==========================================
// 第四步:强行调用私有方法触发业务逻辑 (隔山打牛)
// ========================================== System.out.println("[*] Step 4: 正在强行调用私有方法 openVault()...");
// 获取名为 openVault,且参数类型为 String 的方法
Method openVaultMethod = targetClass.getDeclaredMethod("openVault", String.class);
// 【核心】打破方法的 private 屏障
openVaultMethod.setAccessible(true);
// 执行方法!传入正确的硬编码密码 "HACKED" 来绕过内部的 if 判断
openVaultMethod.invoke(vaultInstance, "HACKED");

System.out.println("[*] 攻击执行完毕!");

} catch (Exception e) {
// 反射操作极易抛出各种异常(如找不到类、找不到方法、非法访问等)
// 实战 Payload 中通常会捕获顶级 Exception 以保证程序不会直接崩溃
System.err.println("[-] 攻击失败,出现异常:");
e.printStackTrace();
}
}
}

简化版执行结果

payload解析

1
2
3
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

导入依赖:在java之中 反射相关的核心类都存放在java.lang.reflect包里面 这三行代码就是我们向系统调用

  • Construcyor(构造器类)
  • Field(属性类/字段类)
  • Method(方法类)
1
2
3
ublic class Main {  

public static void main(String[] args) {

public class ReflectionExploit:定义我们的攻击类 注意!!! 类名必须和文件名一致

public static void main(String[] args):这是 Java 程序的标准入口点。

  • public:保证 JVM 能从外部调用它
  • static:静态方法,意味着 JVM 不需要先 new ReflectionExploit() 就能直接执行它
  • String[] args:接收命令行传入的参数(比如 java ReflectionExploit arg1 arg2
1
2
3
4
5
try {
// ... 攻击代码 ...
} catch (Exception e) {
e.printStackTrace();
}

异常捕获 由于之前我们学到使用Class.forName会产生异常 所以我们在此处需要使用异常捕获

1
Class<?> targetClass = Class.forName("SecretVault");
  • Class<?> targetClass:声明一个类型为Class的变量 <?>是java的泛型语法 意思是“位置变量”因为在这个阶段 编译器只知道你拿到了一个“图纸” 但并不知道这张图纸具体是属于哪种类的 所以用 ? 占位 之后给这个变量起名叫做targetClass
  • Class.forName("SecretVault"):让 JVM 顺着系统的类路径(Classpath)去寻找一个名叫 SecretVault 的类 找到后,把它加载到内存中 并返回这个类的 Class 对象(也就是图纸)
1
Constructor<?> constructor = targetClass.getDeclaredConstructor();
  • Constructor<?>:声明一个用于存放“构造器对象”的变量
  • targetClass.getDeclaredConstructor():在这张图纸上 扫描并抓取那个**“没有参数”**的构造器 不管它是公有还是私有 只要声明了就抓出来
1
constructor.setAccessible(true);

取消对这个构造器的java语言访问检查

1
Object vaultInstance = constructor.newInstance();
  • Object:这里为什么使用Object? 因为在编译器编译这行代码的时候 不知道newInstance会造出个什么玩意
  • constructor.newInstance():正式下达“制造”指令 在内存的堆区强行划出一块地盘 生成一个 SecretVault 对象 并把它的内存地址交给了左边的 vaultInstance 变量
1
Field secretKeyField = targetClass.getDeclaredField("secretKey");
  • Field secretKeyField:声明一个变量存放属性
  • targetClass.getDeclaredField("secretKey"):锁定名称为secretKey的变量实体
1
secretKeyField.setAccessible(true);

消除这个private属性的私有

1
secretKeyField.set(vaultInstance, "CTF_PWNED_BY_REFLECTION");
  • secretKeyField.set(A, B):这是一个赋值操作
    参数A:指定要修改的具体对象的属性
    参数B:要把这个属性改成什么值
1
Method openVaultMethod = targetClass.getDeclaredMethod("openVault", String.class);
  • Method openVaultMethod:声明一个存放方法的变量
  • targetClass.getDeclaredMethod(...):获取这个方法

参数 1 ("openVault"):告诉 JVM 你要找哪个方法。
参数 2 (String.class):告诉 JVM 这个方法接收的参数类型是什么。如果 SecretVault 里面有两个 openVault,一个接收 String,一个接收 int,这个参数就能帮助 JVM 精准定位。

1
openVaultMethod.setAccessible(true);

废除方法的私有属性限制

1
openVaultMethod.invoke(vaultInstance, "HACKED");
  • openVaultMethod.invoke(A, B)invoke 是调用的意思
    参数A:告诉JVM 这个方法属于哪个具体对象的动作 也就是“让 vaultInstance 这个对象去执行 openVault 方法”
    参数B:传给这个方法的真实参数 此处传入HACKED满足判断条件

Class.forName的两个重要参数

1
Class.forName(classname, initialize, currentLoader)
  • 类的全限定名称
  • 是否执行类初始化
  • 指定类加载器

重要结论
initialize = true 并不会执行构造函数,而是执行类的静态初始化块(static {}

类初始化的三个阶段

1
2
3
4
5
6
7
//诸如一下测试类

public class TrainPrint {
{ System.out.printf("Empty block initial %s\n", this.getClass()); }
static { System.out.printf("Static initial %s\n", TrainPrint.class); }
public TrainPrint() { System.out.printf("Initial, %s\n",this.getClass()); }
}

可以看到再这个类之中是由三个板块的 他们的执行顺序是

  1. static {} —— 类加载时执行(仅一次)(静态初始化块)

  2. {} —— 实例初始化块,在构造函数 super() 之后、构造函数内容之前执行(实例初始化块)

  3. 构造函数 —— 最后执行(构造函数)

由此我们可以知道 假设这个类被创造了两个对象 输出结果如图

1
2
3
4
5
Static initial class TrainPrint
Empty block initial class TrainPrint
Initial class TrainPrint
Empty block initial class TrainPrint
Initial class TrainPrint

实例初始化块和构造函数的关系

编译器会将实例初始化的代码块复制到每一个构造函数的最前面(super()之后) 构造函数自己的diamagnetic之前
比如

1
2
3
4
public class Demo {
{ System.out.println("实例块"); }
public Demo() { System.out.println("构造器"); }
}

实际上编译之后等价于

1
2
3
4
5
6
7
public class Demo {
public Demo() {
super();
System.out.println("实例块");
System.out.println("构造器");
}
}

super()是什么

这是java之中调用父类无参构造方法的语句
基本作用

  • 每个子类的构造方法中,第一行必须是 super(...) 或 this(...)
  • 如果程序员没有显式写 super(...);,编译器会自动插入 super();(前提是父类有一个无参构造方法)。
  • super(); 用于先完成父类对象的初始化,再执行子类自己的构造代码。

总结来说这就是java继承体系之中保证对象完整初始化的机制

反射加载内部类

对于单例模式的突破

使用静态方法特性绕过

私有构造方法

我们可能会遇到这样的一种类

  • 只允许全局拥有一个实例
  • 通过静态方法(不需要创建对象就能使用)获取唯一实例
  • 构造方法私有 防止外部new

那么我们使用clazz.newInstance() 由于它只能调用 无参的公共构造方法 就无法实现反射

此时我们就得利用静态方法实现反射

正确反射调用链条

  1. 获取类对象

  2. 获取 静态方法并调用它 → 得到 唯一 实例

  3. 获取 目标普通方法

  4. 在 唯一 实例上调用 目标方法

实例分析

我们选用java.lang.Runtime这个类来进行讲解

java.lang.Runtime简单介绍

这是java编程语言之中的一个重要类 提供了与Java应用程序运行时候的环境交互的方法 通过这个类 开发者可以访问JVM的一些底层功能

  • 执行外部进程
  • 获取内存信息
  • 关闭JVM 这些……
1
2
3
4
5
try {
runtime.exec("notepad.exe");
} catch (IOException e) {
e.printStackTrace();
}

比如这样就能打开记事本

分析选择原因

我们使用一个简单的Runtime单例模式 说明为什么这种情况下反射不能直接newInstance() 以及为什么必须通过getRuntime静态方法获取实例

1
2
3
4
5
6
7
public class Runtime {
private static Runtime currentRuntime = new Runtime();
private Runtime() {}
public static Runtime getRuntime() {
return currentRuntime;
}
}

我们来分析一下这一段代码的实现逻辑

1
private static Runtime currentRuntime;

存放唯一实例 并且设定静态规定这个字段属于类本身 而不是某个实例 内存之中只有一份 所有对象共享他(解决了不能创建对象的困扰)

1
private Runtime() {}

设定一个私有构造器 防止外部new对象

1
2
3
public static Runtime getRuntime() {
return currentRuntime;
}

设定返回值是之前创建的唯一对象
这是一个全局访问点 使用public static打破了私有构造的隔离 安全返回唯一实例的应用 让外部可以无对象获得单例对象

payload构造与解析

1
2
3
4
5
6
7
8
9
10
11
 Class<?> clazz = Class.forName("java.lang.Runtime");

// 1. 获取静态方法 getRuntime 并调用 → 得到 Runtime 对象
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtimeObj = getRuntimeMethod.invoke(null); // 静态方法第一个参数传 null 或 clazz

// 2. 获取普通方法 exec
Method execMethod = clazz.getMethod("exec", String.class);

// 3. 在 runtimeObj 上调用 exec
execMethod.invoke(runtimeObj, "calc.exe");

我们来逐步解析一下这个构造过程

1
Class<?> clazz = Class.forName("java.lang.Runtime");

这一步就是我们的传统方法 利用forName拿到类对象

1
Method getRuntimeMethod = clazz.getMethod("getRuntime");

获取方法getRuntime 此时getRuntimeMethod代表静态方法getRuntime

1
Object runtimeObj = getRuntimeMethod.invoke(null);

调用这个方法 获取唯一实例
这里有一个知识点
invoke
第一个参数标识“调用这个方法的对象

  • 如果方法是静态方法 第一个参数填null(填这个类本身也可以 但是一般都是使用null)
  • 如果方法是普通的 第一个参数填具体的实例对象
    这里由于getRuntime是静态方法 所以我们传入null

invoke之后的返回值就是Runtime.getRuntime() 返回的那个 Runtime 实例
相当于这一步完成了 Runtime rt = Runtime.getRuntime();

1
Method execMethod = clazz.getMethod("exec", String.class);

这一步是获取exec这个普通方法 由于java支持函数重载 所以要指定传参类型

1
execMethod.invoke(runtimeObj, "calc.exe");

依旧是invoke调用这个方法 由于这个方法·此时是普通方法 所以第一个参数传入实例对象(也就是我们用来存放第一个invoke返回结果的对象runtimeobj)第二个参数传入执行命令即可

有参构造 且没有静态获取方法的情况

前两个场景我们解决了私有化和单例化的阻碍 假如目标类既不是单例 构造器也是public 我们还需要反射吗?
答案是需要 因为在安全攻防之中 我们能操控的大多知识用户输入的字符串 而传统的new方法并不能被我们使用 恰恰此时反射由于其将类名以字符串形式传入的方式 能够在此场景下帮助我们 这一点在之后的反序列化攻击之中至关重要

核心方法

与无参构造不同 处理有参构造需要两步精准匹配

  • clazz.getConstructor(Class<?>...parameterTypes):根据传入的参数类型列表定位唯一构造器
  • constructor.newInstance(Object...initargs):创建实例时 传入与构造器参数类型匹配的实际数据
    和我们之前提到的getDeclaredConstructor不同 这个getConstructor只能获取public修饰的构造器

实例分析

我们可以利用java执行系统命令的官方推荐类ProcessBuilder来进行一次测试
这个类有公共构造器

1
2
3
4
5
// 构造器 1:接收一个 List<String> 参数
public ProcessBuilder(List<String> command)

// 构造器 2:接收可变长字符串参数(本质是 String[] 数组)
public ProcessBuilder(String... command)

可以编写一下payload来哦实现反射调用代码

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
package java_reflect;  

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays; // 必须导入!
import java.util.List;

public class ReflectProcessBuilder {
public static void main(String[] args) throws Exception {
// 1. 动态加载类(类名以字符串形式传入)
Class<?> clazz = Class.forName("java.lang.ProcessBuilder");

// 2. 精准定位有参构造器:参数类型是 java.util.List Constructor<?> constructor = clazz.getConstructor(List.class);
// 注意:这里不能写成 getConstructor(String.class),因为该类没有单 String 参数的构造器!

// 3. 传入实际参数,创建实例(此处将命令封装为 List)
Object pbInstance = constructor.newInstance(Arrays.asList("calc.exe"));

// 4. 继续反射调用该实例的普通方法 start() 来执行命令
Method startMethod = clazz.getMethod("start");
startMethod.invoke(pbInstance);

System.out.println("[+] 进程启动成功!");
}
}

小结

到这里我们依据完整学习了java反射的三种场景

  • 私有构造器
  • 私有构造器+单例模式
  • 有参公有构造器

参考文章

相关文章
评论
分享
  • 社会工程学——cupp获取windows密码

    这其实本质是一个社会工程学问题至于为什么 在文章的最后想必大家也能理解假设现在有一个人短时间离开电脑之后并未锁屏 那么旁人就可以通过U盘拷贝重要密码文件的方式获取他的密码 密码文件获取windows并不会直接存储你的明文密码 它存的是...

    社会工程学——cupp获取windows密码
  • URLDNS链入门

    在一些测试环境之中 目标是否发生反序列化是无回显的 是否有一种办法能够测试目标是否能够发生反序列化呢?有的兄弟有的如果我们能让目标服务器通过反序列化发起一次DNS解析请求 那么在接收到请求的时候不就知道了吗那么这就是我们需要学习的UR...

    URLDNS链入门
  • 无痛入门java反序列化

    在学习URLDNS链以及之后的cc链之前 我们得先掌握java反序列化的基础知识 序列化和反序列化序列化把一个java对象转换成字节序列以便存储或者通过网络传输反序列化把字节序列还原成java对象 12序列化:对象->字符串反...

    无痛入门java反序列化
  • 用租房视角学习动态代理

    关于这一部分的知识点 我们先从静态代理说起 静态代理什么是代理? 代理就是:不操作目标对象而是通过一个中间人(代理)来间接操作最常见的现实例子就是租房问题:你要租房子 不需要直接找到房东 而是通过租房中介 中介帮你去和房东沟通 那么...

    用租房视角学习动态代理
  • 动态加载字节码

    动态加载字节码是一种让JVM可以接收由程序在运行时动态产生的字节数组并将其装在的能力 本义是让程序变得更加灵活 但是也产生了一定的安全问题 核心方法在java.lang.ClassLoader类中有一个方法 1protected fi...

    动态加载字节码
  • 类加载机制漫谈

    解释java字节码为什么需要编译成class文件我们首先要理解JVMjava是是一种混合型语言有编译的步骤也有解释步骤我们编写的java文件需要被编译成class文件之后才能被JVM运行 什么是class文件传统编译型语言(C C++...

    类加载机制漫谈
  • LangChain反序列化

    最近在打SCTF的时候遇到一个LangChain反序列化的题目 比较新颖 于是想着学习一下 LangChain介绍LangChain是一个用于构建大语言模型应用的开源框架 通过这个可以把常用的组件抽象成可串联的链(Chain)举个例...

    LangChain反序列化
  • filter-chain漏洞演示

    这个漏洞链被称为CNEXT 由安全员Charles Fol披露 核心思想是利用系统内置的看似无害的模块(流过滤器)的巧妙组合 在受限的web环境之中执行命令 原理讲解filter协议对于filter伪协议 我们肯定都不陌生这个伪协议允...

    filter-chain漏洞演示
  • 死亡绕过

    前言以及环境介绍本文是对p神文章谈一谈php://filter的妙用 | 离别歌知识点的学习 本文主要介绍使用filter伪协议进行死亡绕过的三种方式 测试环境:小皮面板测试代码:exit.php 12345<...

    死亡绕过
  • Phar反序列化漏洞演示

    前言我们在之前学习的反序列化漏洞 无一不是利用了unserialize()这个函数 但是我们没有去考虑 假设此时的题目并没有给我们这个函数 我们还能否实现反序列化呢 其实是可以的 在2018年的BlackHat 大会上 Sam Tho...

    Phar反序列化漏洞演示
Please check the parameter of comment in config.yml of hexo-theme-Annie!