Java反序列化之Java反射进阶

该博客为参考学习笔记博客,仅为本人记录的笔记,所以欢迎大家去Drunkbaby师傅的博客中进行学习!Drunkbaby’s Blog

参考链接:Java反序列化基础篇-03-Java反射进阶

一、反射的进阶知识

1.1 关于Java的java.lang.Runtime

前面我们对这个类进行了一些简单的介绍:

在Java编程中,Runtime 类提供了一种与Java应用程序的运行环境进行交互的方式。Runtime 类是一个单例类,它封装了应用程序运行时的环境,通过它,开发者可以访问JVM的某些底层特性和功能。以下是 Runtime 类的主要作用和功能:

  1. 执行系统命令

    可以使用 exec 方法来执行操作系统命令,这在需要与系统进程交互时非常有用。

    1
    2
    3
    4
    5
    try {
    Runtime.getRuntime().exec("notepad");
    } catch (IOException e) {
    e.printStackTrace();
    }
  2. 内存管理

  3. 关闭JVM

  4. 添加JVM关闭钩子

对于一些RCE来说,这个类就是用来执行命令的;所以在反序列化中大多数的利用都与该类有关,因为可以利用exec方法来执行系统命令;

1.2 设置setAccessible(true)暴力访问权限

在一般情况下,我们使用反射机制不能对类的私有 private 字段进行操作,所以需要通过设置访问权限setAccessible(true)来绕过访问限制从而能够执行一些函数或者修改一些私有变量的值

在此之前我们进行过一个弹计算器的实践,在那里我们就有利用过这个方法,从而能够调用私有的构造函数实现实例化对象;如下:

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

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* @Project unSerialize
* @File com.mango.reflection.reflectionRuntime.java
* @Author mango
* @Date 2024/7/30 13:24
* @Description
*/

public class reflectionRuntime {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException {
Class c = Class.forName("java.lang.Runtime");
Constructor con = c.getDeclaredConstructor(); // 获取构造函数
con.setAccessible(true); // 设置可访问该私有构造函数
Object o = con.newInstance(); // 调用该私有构造函数创建对象
Method mRuntime = c.getDeclaredMethod("getRuntime");
Method mExec = c.getDeclaredMethod("exec", String.class);
Object re = mRuntime.invoke(o);
mExec.invoke(re, "calc");
}
}

1.3 forName的两个重载方法

对于Class.forName()方法,有两个重载方法;

1
2
forName(String className)
forName(String name, boolean initialize, ClassLoader loader)
  • 第一个参数表示类名
  • 第二个参数表示是否初始化
  • 第三个参数表示类加载器,即告诉Java虚拟机如何加载这个类,Java默认的ClassLoader就是根据类名来加载类, 这个类名是类完整路路径,如 java.lang.Runtime

因此,forName(className)等价于forName(className, true, currentLoader)

1.4 各种代码块执行顺序

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

/**
* @Project unSerialize
* @File com.mango.reflectionDemo.FunctionSort.java
* @Author mango
* @Date 2024/7/31 12:16
* @Description
*/

public class FunctionSort {
public static void main(String[] args) {
Test test = new Test();
}
}

class Test{
{
System.out.println("1");
}
static {
System.out.println("2");
}
Test(){
System.out.println("3");
}
}

运行结果:

image-20240731121800676

根据运行的结果可以知道,首先调用的是static{},其次是{},最后才调用构造函数Test()

其中,static()就是在“类初始化”的时候调用的,而{}中的代码回放在构造函数的super()后面,但在当前构造函数内容的前面。

所以说,forName中的initialize=true其实就是告诉Java虚拟机是否执行“类初始化”(是否执行static()中的内容)。

那么,假设我们有如下函数,其中函数的参数name可控:

1
2
3
public void ref(String name) throws Exception {
Class.forName(name);
}

由于默认的initialize参数默认为true,我们即可编写一个恶意类,将恶意代码放置在static()中,使类默认初始化进而执行static()中的参数;

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

import java.io.IOException;

/**
* @Project unSerialize
* @File com.mango.maliciousClass.returnCalc.java
* @Author mango
* @Date 2024/8/2 13:41
* @Description
*/

public class returnCalc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.mango.reflectionDemo;

/**
* @Project unSerialize
* @File com.mango.reflectionDemo.staticReflection.java
* @Author mango
* @Date 2024/8/2 13:42
* @Description
*/

public class staticReflection {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("com.mango.maliciousClass.returnCalc");
}
}

二、Java命令执行的三种方式

反序列化当中需要入口类,需要链子,还需要一个命令执行的方法。

2.1 调用 Runtime 类进行命令执行

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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
* @Project unSerialize
* @File com.mango.maliciousClass.runtimeCE.java
* @Author mango
* @Date 2024/8/2 13:55
* @Description
*/

public class runtimeCE {
public static void main(String[] args) throws IOException {
InputStream inputStream = Runtime.getRuntime().exec("whoami").getInputStream();
byte[] cache = new byte[1024];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int readLen = 0;
while ((readLen= inputStream.read(cache))!=-1)
byteArrayOutputStream.write(cache, 0, readLen);
System.out.println(byteArrayOutputStream);
}
}

大致思路:

  1. 先调用 getRuntime() 返回一个 Runtime 对象,然后调用 Runtime 对象的 exec 的方法。
  2. 调用 Runtime 对象的 exec 的方法会返回 Process 对象,调用 Process 对象的 getInputStream() 方法。
  3. 调用 Process 对象的 getInputStream() 方法,此时,子进程已经执行了 whoami 命令作为子进程的输出,将这一段输出作为输入流传入 inputStream
  • OK,我们的第一行就是用来执行命令的,但是我们执行命令需要得到命令的结果,所以需要将结果存储到字节数组当中
1
2
3
4
int readLen = 0;	//存储每次读取输入流的长度
while ((readLen = inputStream.read(cache))!=-1)
byteArrayOutputStream.write(cache, 0, readLen);
System.out.println(byteArrayOutputStream);

上述代码的作用是将命令执行的结果输出到标准输出中;

2.2 调用 ProcessBuilder 类进行命令执行

通过另一种方式执行命令再通过相同的方式将命令输出内容输出到标准输出;

ProcessBuilder processBuilder = new ProcessBuilder("命令", "参数1", "参数2");

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 com.mango.maliciousClass;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
* @Project unSerialize
* @File com.mango.maliciousClass.processBuilderCE.java
* @Author mango
* @Date 2024/8/2 21:47
* @Description
*/

public class processBuilderCE {
public static void main(String[] args) throws IOException {
InputStream inputStream = new ProcessBuilder("calc").start().getInputStream();
byte[] cache = new byte[1024];
int readLen = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while((readLen=inputStream.read(cache))!=-1)
byteArrayOutputStream.write(cache, 0, readLen);
System.out.println(byteArrayOutputStream);
}
}

2.3 利用反射调用 ProcessImpl 类进行命令执行

ProcessImpl类是更为底层的实现,RuntimeProcessBuilder执行命令实际上也是调用ProcessImpl这个类来实现的。对于ProcessImpl类我们不能直接调用,但是可以通过反射来间接调用ProcessImpl来实现执行命令的目的。

  • 不能直接调用ProcessImpl是因为该类的构造方法是私有的,我们不能直接调用构造方法实例化对象,所以需要通过反射去进行命令执行

image-20240802222011848

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

import javax.activation.MimetypesFileTypeMap;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

/**
* @Project unSerialize
* @File com.mango.maliciousClass.processImplCE.java
* @Author mango
* @Date 2024/8/2 22:16
* @Description
*/

public class processImplCE {
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, IOException, NoSuchMethodException {
Class<?> processImplClass = Class.forName("java.lang.ProcessImpl");
String[] cmds = new String[]{"whoami"};
Method method = processImplClass.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process e = (Process) method.invoke(null, cmds, null, ".", null, true);
InputStream inputStream = e.getInputStream();
byte[] cache = new byte[1024];
int readLen = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while((readLen=inputStream.read(cache))!=-1)
byteArrayOutputStream.write(cache, 0, readLen);
System.out.println(byteArrayOutputStream);
}
}

三、Java反射各种修饰符字段

3.1 private

这个修饰符就不必多说了,直接使用getDeclaredField获取相应变量,再通设置访问权限setAccessible去修改即可;

这里就不在做代码演示了,之前写过比较多类似的代码了。

3.2 static

单单一个static修饰符静态变量,跟private是一致的。这里把Drunkbaby师傅的代码粘贴过来

1
2
3
4
5
6
7
8
public class StaticPerson {  
private static StringBuilder name = new StringBuilder("Drunkbaby");

public void printInfo() {
System.out.println(name);

}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class StaticReflect {  
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.StaticPerson");
Object m = c.newInstance();
Method nameMethod = c.getDeclaredMethod("printInfo");
nameMethod.invoke(m);
Field nameField = c.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(m,new StringBuilder("Drunkbaby static Silly"));
nameMethod.invoke(m);
}
}

3.3 final

  1. final变量直接复制

经过本人的一些尝试后发现确实无法修改及时绕过报错的问题,但是仍然无法做到对变量的修改;

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
58
59
60
61
package com.mango.entity;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
* @Project unSerialize
* @File com.mango.entity.Student.java
* @Author mango
* @Date 2024/8/2 22:49
* @Description
*/

public class Student {
private String name = "mango";
private static String gender = "male";
private final int age = 18;

public Student(String name) {
this.name = name;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", gender='" + gender + '\'' +
", age=" + age +
'}';
}
public static void main(String[] args) {
try {
// 创建 Student 类的对象
Student student = new Student("John");
System.out.println("Before: " + student);

// 获取 Student 类的 Class 对象
Class<?> clazz = Student.class;

// 获取 Student 类中的 final 变量 age
Field ageField = clazz.getDeclaredField("age");

// 将 ageField 设置为可访问
ageField.setAccessible(true);

// 获取 Field 的 modifiers 属性的 Field 对象
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);

// 修改 modifiers 属性,取消 final 修饰符
modifiersField.setInt(ageField, ageField.getModifiers() & ~Modifier.FINAL);

// 修改 final 变量的值
ageField.setInt(student, 20);

System.out.println("After: " + student);
} catch (Exception e) {
e.printStackTrace();
}
}
}
  1. final变量间接赋值

若是final变量通过简介赋值,则只需要通过与private修饰符修饰的变量一样的方法即可修改变量的值

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

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
* @Project unSerialize
* @File com.mango.entity.Student.java
* @Author mango
* @Date 2024/8/2 22:49
* @Description
*/

public class Student {
private String name = "mango";
private static String gender = "male";
private final int age;

public Student(String name, int age) {
this.name = name;
this.age = 18;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", gender='" + gender + '\'' +
", age=" + age +
'}';
}
public static void main(String[] args) {
try {
// 创建 Student 类的对象
Student student = new Student("John", 18);
System.out.println("Before: " + student);

// 获取 Student 类的 Class 对象
Class<?> clazz = Student.class;

// 获取 Student 类中的 final 变量 age
Field ageField = clazz.getDeclaredField("age");

// 将 ageField 设置为可访问
ageField.setAccessible(true);


// 修改 final 变量的值
ageField.setInt(student, 20);

System.out.println("After: " + student);
} catch (Exception e) {
e.printStackTrace();
}
}
}

3.4 static + final

static + final终究也还是个final,若是该变量经过直接赋值的方式,那么变量的值便无法修改啦,通过反射机制也无法修改。

若是简介赋值则可以通过反射机制取消掉final修饰符再修改赋值即可

1
2
3
4
5
6
7
8
public class StaticFinalPerson {  
static final StringBuilder name = new StringBuilder("Drunkbaby");

public void printInfo() {
System.out.println(name);

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StaticFinalReflect {  
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.StaticFinalPerson");
Object m = c.newInstance();
Method printMethod = c.getDeclaredMethod("printInfo");
printMethod.invoke(m);

// 获取StaticFinalPerson类中的 static final 变量 name
Field nameField = c.getDeclaredField("name");
// 设置 nameField 可访问
nameField.setAccessible(true);
// 获取 nameField 的 modifiers 属性的 Field 对象
Field nameModifyField = nameField.getClass().getDeclaredField("modifiers");
// 将 nameModifyField 设置可访问
nameModifyField.setAccessible(true);
// 修改 modifiers 属性,取消 final 修饰符
nameModifyField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL);
nameField.set(m,new StringBuilder("Drunkbaby Too Silly"));
// 修改 modifiers 属性,取消 final 修饰符
nameModifyField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL);
printMethod.invoke(m);
}
}

Java反序列化之Java反射进阶
http://candyb0x.github.io/2024/08/02/Java反序列化之Java反射进阶/
作者
Candy
发布于
2024年8月2日
更新于
2024年8月2日
许可协议