Java安全学习

Java安全学习

只开新坑不填旧坑这一块

反射

反射是Java中很重要的一个特性,我们可以使用反射来动态获取一个对象的所有信息。

所以,反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。 ————廖雪峰的官方网站

我们都知道Java作为面向对象语言中最具代表性的一个,在Java中一切皆对象。而每个对象都对应着用来实例化它的类。

如果不用反射或类似的运行时发现机制,程序员必须在编译期/代码中显式绑定类型与实例化逻辑(写 new、写工厂、写注册代码),因此替换实现或动态加载插件时需要改大量代码或依赖构建时/配置时的额外步骤;反射把“选择实现/方法”的时机从编译期延后到运行期,从而降低了代码耦合和样板,但会以运行时安全性、性能和可维护性为代价。。

Java中的注解就是使用反射来实现的,通过反射来获取类并遍历各个类中的方法,从而找到所有想要的方法并注入。以Spring为例,当你在 Spring 配置文件中或者使用注解 @Autowired 时,你只需要告诉 Spring:“我需要一个 UserService 的对象”。Spring 容器在启动时,会读取你的配置,然后通过反射机制,动态地找到 UserService 这个类,调用它的构造函数创建一个实例,然后把它注入到你需要的地方。你并没有写 new UserService() 这行代码,是 Spring 通过反射帮你做了。

如对象序列化 / 反序列化(将对象转为 JSON 或 XML)、ORM(对象关系映射)框架,都需要反射来工作。

举例:JSON 序列化库 (如 Jackson, Gson)

当你调用 new Gson().toJson(myObject) 时,Gson 库并不知道 myObject具体是什么类,有哪些字段。它会使用反射机制,遍历 myObject对象的所有字段(包括 private的),获取它们的名称和值,然后将其组织成 JSON 字符串。

总而言之,有了反射机制,在调用某个对象的方法或访问其字段时,都不再需要知道这个对象对应的类的全部信息,只需要拿到一个实例,就可以尽情地使用它。

Java 的反射 API 提供了一系列的类和接口来操作 Class 对象。主要的类包括:

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

一般来说,反射的工作流程是:获取Class对象=>获取成员信息(通过Class获取字段、方法、构造函数等)=>操作成员(读取或者修改值、调用方法、创建对象等)

获取对象

一般使用**Class.forName()来获取对象,使用Class.forName()**获取对象非常简单,我们只需要知道一个类的类名,就可以获取到这个类的对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package test;

public class test {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("java.lang.String");
        System.out.println("Class obtained using Class.forName(): " + clazz.getName());
    }
}

// Class obtained using Class.forName(): java.lang.String

我们现在知道了有反射,同时知道有个Runtime类可以执行命令,那两者结合,通过这种动态调用的形式就可以执行命令。

Java中有三种代码块:静态初始化块(static block)实例初始化块(instance initializer block)构造方法(constructor)

 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 test;

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

class Test1 {
    public Test1() {
        System.out.println("我是构造方法");
    }

    static {
        System.out.println("我是静态初始化块");
    }

    {
        System.out.println("我是实例初始化块");
    }
}

/* 我是静态初始化块
   我是实例初始化块
   我是构造方法    */
  • 静态初始化块(static {})在类被加载到 JVM 时执行,仅执行一次,只能访问静态成员,同时在任何对象创建之前执行。
  • 实例初始化块({})每次创建对象时,在构造方法之前执行。
  • 构造方法(public ClassName() {})每次创建对象时,在实例初始化块之后执行。

可以看到静态初始化块中的代码最先执行,构造方法最后执行。而**Class.forName()**就是用来控制静态初始化块的,所以它的优先级非常高。可以往进塞恶意代码:

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

public class test {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("test.Test1");
    }
}

class Test1 {
    public Test1() {
        System.out.println("我是构造方法");
    }

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

    {
        System.out.println("我是实例初始化块");
    }
}

// hacked

创建对象

就是使用getDeclaredConstructor()来创建实例:

 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 test;

public class test {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("test.Test1");
        Object obj = clazz.getDeclaredConstructor().newInstance();
    }
}

class Test1 {
    public Test1() {
        System.out.println("我是构造方法");
    }

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

    {
        System.out.println("我是实例初始化块");
    }
}

/* hacked
   我是实例初始化块
   我是构造方法  */

访问字段

可以使用这些方法来访问字段:

clazz.getField(String name):获取公有字段(包括继承的)

clazz.getDeclaredField(String name):获取本类中声明的字段(包括私有字段)

field.setAccessible(true):允许访问私有字段

field.get(Object obj):获取字段值

field.set(Object obj, Object value):设置字段值

代码:

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

import java.lang.reflect.Field;

public class test {
    public static void main(String[] args) throws Exception {
        // 获取类
        Class<?> clazz = Class.forName("test.Cat");
        // 实例化类(创建对象)
        Cat cat = (Cat) clazz.newInstance();
        // 访问公有字段
        Field publicCat = clazz.getField("name");
        System.out.println("Get Public Field CatName is: " + publicCat.get(cat));
        publicCat.set(cat, "耄耋");
        System.out.println("Get Public Field CatName is: " + publicCat.get(cat));
        // 访问私有字段
        Field privateCat = clazz.getDeclaredField("age");
        privateCat.setAccessible(true);
        System.out.println("Get Private Field Catage is: " + privateCat.get(cat));
        privateCat.set(cat, 10);
        System.out.println("Get Private Field Catage is: " + privateCat.get(cat));
    }
}

class Cat {
    public String name = "哈基米";
    private int age = 2;
}

/* Get Public Field CatName is: 哈基米
   Get Public Field CatName is: 耄耋
   Get Private Field Catage is: 2
   Get Private Field Catage is: 10 */

调用方法

调用方法和访问字段的操作差不多,具体流程为:

获取 Class =>

获取目标 Method 对象(公有用 getMethod,私有用 getDeclaredMethod)=>

设置可访问(私有方法要 setAccessible(true))=>

使用 invoke() 调用方法

可以使用这些来获取:

clazz.getMethod(String name, Class… parameterTypes):获取 公有方法(包括继承的)(注意,这里第一个参数是方法名,后面的参数表示的是这个方法传入参数的类型,比如参数是String就是String.class)

clazz.getDeclaredMethod(String name, Class… parameterTypes):获取 本类声明的方法(包括私有方法)

method.setAccessible(true):允许访问私有方法

method.invoke(Object obj, Object… args):调用方法,第一个参数是对象,后面是传入方法的参数

代码例子:

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

import java.lang.reflect.Method;

public class test {
    public static void main(String[] args) throws Exception {
        // 获取类
        Class<?> clazz = Class.forName("test.Cat");
        // 实例化类(创建对象)
        Cat cat = (Cat) clazz.newInstance();
        // 调用公有方法
        Method PublicCat = clazz.getMethod("Ha", String.class);
        PublicCat.invoke(cat, "人? ");
        // 调用私有方法
        Method privateCat = clazz.getDeclaredMethod("add", int.class, int.class);
        privateCat.setAccessible(true);
        int result = (int) privateCat.invoke(cat, 1, 1);
        System.out.println("强化普攻次数: " + result);
    }
}

class Cat {
    public void Ha(String name) {
        System.out.println(name + "哈!");
    }

    private int add(int a, int b) {
        return a + b; // 计算挠人次数(不是)
    }
}

/* 人? 哈!
   强化普攻次数: 2   */

如果类中存在静态方法,那我们就不需要再实例化对象了,可以直接操作类

就像这样:

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

import java.lang.reflect.Method;

public class test {
    public static void main(String[] args) throws Exception {
        Method staticMethod = Cat.class.getMethod("say", String.class);
        String echoed = (String) staticMethod.invoke(null, "曼波");
        System.out.println(echoed);
    }
}

class Cat {
    public static String say(String name) {
        return "Hello " + name;
    }
}


// Hello 曼波 

如果方法是静态方法,invoke() 的第一个参数可以传 null,毕竟这个时候我们也没有具体的对象。

现在让我们来弹个计算器

1
2
3
4
5
6
7
8
package test;

public class test {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("java.lang.Runtime");
        clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");
    }
}

其中,clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");

我们可以把它看作是一个嵌套的方法调用:

A.invoke(B, "calc.exe"),其中:

  • Aclazz.getMethod("exec", String.class) 的结果。
  • Bclazz.getMethod("getRuntime").invoke(clazz) 的结果。

根据规则,JVM 要执行 A.invoke(...),必须先计算出 AB 的值。

  1. 计算 A 的值: 执行 clazz.getMethod("exec", String.class)
    • 这本身又是一个方法调用 (getMethod),JVM 会先计算它的参数("exec"String.class),然后执行 getMethod,返回一个 Method 对象(代表 exec 方法)。现在 A 的值确定了。
  2. 计算 B 的值: 执行 clazz.getMethod("getRuntime").invoke(clazz)
    • 这是一个链式调用,我们可以把它拆成 C.invoke(clazz),其中 Cclazz.getMethod("getRuntime") 的结果。
    • 根据规则,要执行 C.invoke(...),必须先计算 C 的值。
    • 计算 C 的值: 执行 clazz.getMethod("getRuntime"),返回一个 Method 对象(代表 getRuntime 方法)。现在 C 的值确定了。
    • 执行 C.invoke(clazz): 现在参数都齐了,可以执行 invoke 方法了。这会调用 Runtime.getRuntime(),返回一个 Runtime 实例。现在 B 的值也确定了。
  3. 执行 A.invoke(B, "calc.exe"):
    • 现在,invoke 方法的所有参数(B"calc.exe")都计算完毕。
    • JVM 终于可以执行 A 所代表的 exec 方法的 invoke 调用了,这会调用 runtimeInstance.exec("calc.exe")

原因是在Java中,方法调用的参数,总是在方法本身执行之前被计算

所以上面的代码展开是这样的:

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

import java.lang.reflect.Method;

public class test {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("java.lang.Runtime");
        Method getRuntimeMethod = clazz.getMethod("getRuntime");
        Object runtime = getRuntimeMethod.invoke(null);
        Method execMethod = clazz.getMethod("exec", String.class);
        execMethod.invoke(runtime, "calc.exe");
    }
}

非无参构造方法和私有方法的调用

我们知道,我们想要调用一个类的方法执行函数,一般情况下都要获取到相应的实例对象,然后调用实例对象的方法函数

上面的例子中,由于没有重载构造方法,所以我们可以直接调用默认的无参构造方法来实例化对象;或是使用了单例模式,使我们得以调用静态方法来获取实例对象。

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

另外,如果我们想调用的方法是私有方法,要怎么调用呢?

非无参构造方法

要解决这个问题需要使用getConstructors()方法,这里做一下补充:

java.lang.reflect

java.lang.reflect 是 Java 反射机制的核心包,提供了操作类及其成员(字段、方法、构造函数等)的类和接口。通过这些 API,开发者可以在运行时动态地查询和修改类的结构。

1. Class

  • 功能:表示类的对象,提供了获取类信息的方法,如字段、方法、构造函数等。

  • 主要方法

    • getFields():获取所有公共字段。
    • getDeclaredFields():获取所有声明的字段,包括私有字段。
    • getMethods():获取所有公共方法。
    • getDeclaredMethods():获取所有声明的方法,包括私有方法。
    • getConstructors():获取所有公共构造函数。
    • getDeclaredConstructors():获取所有声明的构造函数,包括私有构造函数。
    • getSuperclass():获取类的父类。
    • getInterfaces():获取类实现的所有接口。

2. Field

  • 功能:表示类的字段(属性),提供了访问和修改字段值的方法。

  • 主要方法

    • get(Object obj):获取指定对象的字段值。
    • set(Object obj, Object value):设置指定对象的字段值。
    • getType():获取字段的数据类型。
    • getModifiers():获取字段的修饰符(如 public、private)。

3. Method

  • 功能:表示类的方法,提供了调用方法的能力。

  • 主要方法

    • invoke(Object obj, Object... args):调用指定对象的方法。
    • getReturnType():获取方法的返回类型。
    • getParameterTypes():获取方法的参数类型。
    • getModifiers():获取方法的修饰符(如 public、private)。

4. Constructor

  • 功能:表示类的构造函数,提供了创建对象的能力。

  • 主要方法

    • newInstance(Object... initargs):创建一个新实例,使用指定的构造函数参数。
    • getParameterTypes():获取构造函数的参数类型。
    • getModifiers():获取构造函数的修饰符(如 public、private)。

getConstructor 接收的参数是构造函数列表类型,因为构造函数支持重载,所以只能通过参数列表来唯一确定一个构造函数。

用ProcessBuilder执行命令来举例:

ProcessBuilder有两个构造函数:

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

我们只关注使用反射的方式来获取构造方法:

调用第一个构造函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package test;

import java.util.Arrays;
import java.util.List;

public class test {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
        clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));
    }
}

调用第二个构造函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package test;

import java.util.Arrays;
import java.util.List;

public class test {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
        clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));
    }
}

想通过第二种构造函数实现RCE,这里就需要用到可变长参数了,用...表示参数可变,而由于编译时可变长参数其实会被编译成数组,所以String[]和String…其实没有什么差别

私有方法调用

私有方法只需要setAccessible(true)就行:

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

import java.lang.reflect.Constructor;

public class Java05_Reflect_test {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("java.lang.Runtime");
        Constructor m = clazz.getDeclaredConstructor();
        m.setAccessible(true);
        clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");
    }
}

不过这个在Java9及以上版本会报错

Licensed under CC BY-NC-SA 4.0
Build by Oight
使用 Hugo 构建
主题 StackJimmy 设计