0x00 前言

Java反序列化基础篇的第三篇文章。

参考教程:
白日梦组长的视频 Java反序列化漏洞专题

Java反序列化基础篇-03-Java反射进阶 | Drunkbaby’s Blog

0x01 知识回顾

setAccessible(true)

  • setAccessible(),即暴力访问权限。当一个类的构造器、属性、方法被private修饰时,就需要设置setAccessible(true),才能成功访问它们。

  • 这种方法可以和getConstructor配合使用,再回顾一下getConstructor方法:

image-20241108112135005

该方法可以拿到指定的Class对象的构造器。

利用思路:(顺序并不绝对,看个人理解和习惯)

  1. 先得到Class对象
  2. 再从Class对象拿取exec方法
  3. 实例化Class对象
  4. 利用exec方法

代码实现如下:

image-20241108114338339

但是可以实现的更简单一点:

image-20241108114645805

就其原理,还是前文所述,利用反射机制得以利用类的方法,该方法的实现需要类的实例,类的实例的实现需要构造器,所以就需要getConstructor,本例访问的构造器需要权限,所以使用setAccessible(true),这就是二者配合实现exec

forName 的重载方法

Class.forName() 有两个重载方法,如下:

image-20241108115305731

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

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

这里要注意是currentLoadercurrentLoader 是指调用 forName 方法的类的类加载器,而不是别的类加载器

各种代码块执行顺序

先看下面代码,注意各个代码块:

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

import sun.misc.Unsafe;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

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

{
System.out.println("1");
}

static {
System.out.println("2");
}

Test() {
System.out.println("3");
}
}
}


输出:

image-20241108120219271

也就是说先调用static {}块,再调用{}块,最后调用当前构造函数内容,这是为什么呢?

static {}块在类初始化的时候调用,{}块在构造函数的super()后但在当前构造函数内容的前⾯调用,最后才是调用当前构造函数内容。

拓展

forName 中的 initialize=true 意味着目标类将会初始化,先看如下代码:(参数name可控)

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

那我们只要把恶意类写在目标类(name)的static {}块即可在目标类初始化时调用恶意代码了。

0x02 Java命令执行的三种反射

Java反序列化的三步就是:入口类、链子和命令执行的方法。下面讲讲Java命令执行的三种方法:

(1)Runtime

使用:

image-20241110153118917

思路:

将执行结果的输出流存储在自定义的数组,再把数组的数据读出来。下面是更详细的解释:

  1. 先调用 getRuntime() 返回一个 Runtime 对象,然后调用 Runtime 对象的 exec 的方法。
  2. 调用 Runtime 对象的 exec 的方法会返回 Process 对象,调用 Process 对象的 getInputStream() 方法。
  3. 调用 Process 对象的 getInputStream() 方法,此时,子进程已经执行了 whoami 命令作为子进程的输出,将这一段输出作为输入流传入 inputStream
  • OK,我们的第一行就是用来执行命令的,但是我们执行命令需要得到命令的结果,所以需要将结果存储到字节数组当中

这一段代码用来保存运行结果

JAVA

1
2
3
4
5
6
7
8
9
byte[] cache = new byte[1024];  
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
/**
* readLen用于存储每次读取输入流的长度
*/
int readLen = 0;
while ((readLen = inputStream.read(cache))!=-1){
byteArrayOutputStream.write(cache, 0, readLen);
}

(2)ProcessBuilder

与Runtime类唯一的区别如下:

image-20241110153834626

(3)ProcessImpl

前面的RuntimeProcessBuilder执行命令实际上还是调用了ProcessImpl这个类:

image-20241110160144828

跟进ProcessImpl类:

image-20241110160301152

构造方法是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
package com.pax.UnserializeBlog;

import sun.misc.Unsafe;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;

public class LocalClassTest {
public static void main(String[] args) throws Exception {
Class c = Class.forName("java.lang.ProcessImpl");
Method method = c.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
String[] cmds = new String[]{"whoami"};
Process process = (Process) method.invoke(null, cmds, null, ".", null, true);
InputStream inputStream = process.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);
}
}

0x03 Java反射修改static final修饰的字段

private

如果类成员变量被private修饰,使用getDeclaredField方法即可

static

如果static 单独出现的话,getDeclaredField 也是可以的

final

final字段能否修改,取决于字段是直接赋值还是间接赋值(编译时赋值还是运行时赋值)

直接赋值是指在创建字段时就对字段进行赋值,并且值为 JAVA 的 8 种基础数据类型或者 String 类型,而且值不能是经过逻辑判断产生的,其他情况均为间接赋值。

直接赋值

写一个Man类:

image-20241110164118945

此时Man类的nameage都是直接赋值,反射结果如下:

image-20241110164305067

结果是没变的,说明反射修改字段失败

间接赋值

修改一下Man类:

image-20241110164810348

结果是成功的:

image-20241110164920343

小结一下:

如果被final修饰的变量是间接赋值也就是在运行时赋值,就可以进行反射调用。

static + final

使用 static final 修饰符的 name 属性,并且是间接赋值,直接通过反射修改是不可以的。师傅们可以自行尝试,这里我们需要通过反射, 把 nameField 的 final 修饰符去掉,再赋值。

修改Man类:

image-20241110165236124

给出方法:

image-20241110170503162

解析:

modifiers 是一个包含字段修饰符的内部字段(例如 privatefinal 等)

通过 nameField.getClass().getDeclaredField("modifiers") 获取 modifiers 字段。

将其设置为可访问。

使用 nameModifyField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL) 来移除 final 标志。

此时 name 字段已经不再是 final,可以用 nameField.set(man, new StringBuilder("manbaout")) 来修改 name 字段的值。

简而言之,先获取modifiers 字段,对modifiers 字段进行final修饰符去除,然后再设置新值。

0x04 结语

本篇的知识还是比较多的,好好消化吧,加油!