抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Qingwan

在时间的维度上,一切问题都是有解的。

这个系列的博客主要是在看P神的安全漫谈之后的一些笔记还有对应一些地方的扩展以及自己的了解。

Java基础知识

在学习反射之前,我们先了解一下关于 java 的基础知识

  1. java运行的全过程

    Java源代码—->编译器—->jvm可执行的Java字节码(即虚拟指令)—->jvm—->jvm中解释器—–>机器可执行的二进制机器码—->程序运行。

  2. java和c++的区别

  • 都是面向对象的语言,都支持封装、继承和多态
  • Java不提供指针来直接访问内存,程序内存更加安全
  • Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。
  • Java有自动内存管理机制,不需要程序员手动释放无用内存

基础语法

  1. 数据类型
    ①基本数据类型
  • 数值型
    • 整数类型(byte,short,int,long)
    • 浮点类型(float,double)
  • 字符型(char)
  • 布尔型(boolean)
    ②引用数据类型
    引用数据类型
  • 类(class)
  • 接口(interface)
  • 数组([])
  1. 访问修饰符
修饰符 当前类 同包 子类 其他包
private × × ×
default × ×
protected ×
public
  1. this关键字的用法
    this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
    this的用法在java中大体可以分为3种:
    ① 普通的直接引用,this相当于是指向当前对象本身。

② 形参与成员名字重名,用this来区分:
如:

1
2
3
4
public Person(String name, int age) {
this.name = name;
this.age = age;
}

③ 引用本类的构造函数,当为这个作用时
this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person{
private String name;
private int age;

public Person() {
}

public Person(String name) {
this.name = name;
}
public Person(String name, int age) {
//调用其他的构造方法
this(name);
this.age = age;
}
}

this用处大概可以总结到一段代码里

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
62
63
public class Person {
private String name;

// 构造方法1:初始化姓名
public Person(String name) {
this.name = name; // 使用this关键字区分实例变量和局部变量
}

// 构造方法2:不带参数
public Person() {
this("Unknown"); // 调用另一个构造方法初始化姓名
}

// 获取姓名
public String getName() {
return this.name; // 返回实例变量name
}

// 设置姓名
public void setName(String name) {
this.name = name; // 使用this关键字指代当前对象的实例变量
}

// 打印个人信息
public void printInfo() {
System.out.println("Name: " + this.name);
}

// 返回当前对象的引用
public Person getSelf() {
return this;
}

// 使用当前对象调用其他方法
public void doSomething() {
// do something
}
}

public class Main {
public static void main(String[] args) {
// 创建Person对象
Person person1 = new Person("Alice");

// 使用构造方法2创建Person对象
Person person2 = new Person();

// 输出姓名
System.out.println("Person 1 Name: " + person1.getName());
System.out.println("Person 2 Name: " + person2.getName());

// 设置姓名
person1.setName("Bob");
System.out.println("Person 1 Name after modification: " + person1.getName());

// 返回当前对象的引用
Person self = person1.getSelf();

// 调用其他方法
self.doSomething();
}
}

反射

  1. 什么是反射?反射的作用?
    Java反射(Reflection)是Java编程语言中的一个强大特性,它允许程序在运行时检查和修改类、方法、字段和接口。这意味着你可以在程序运行时获取类的信息,创建对象,调用方法,甚至修改字段值,这一切都可以动态地进行。
    定义:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取、调用对象方法的功能称为java语言的反射机制,程序在运行期可以拿到一个对象的所有信息。。

反射是通过Class对象(字节码文件),来知道某个类的所有属性和方法。也就是说通过反射我们可以获取构造器,对象,属性,方法。在JAVA框架中,很多类我们是看不见的,不能直接用类名去获取对象,只能通过反射去获取。

对象可以通过反射获取他的类,类可以通过反射拿到所有⽅法(包括私有),拿到的⽅法可以调⽤,总之通过“反射”,我们可以将Java这种静态语⾔附加上动态特性。

动态特性:“⼀段代码,改变其中的变量,将会导致这段代码产⽣功能性的变化,称之为动态特性”。

在Java中,每个类都有一个与之关联的Class对象。你可以通过以下几种方式获取Class对象:

  • 使用Class.forName()方法,传入类的全路径名称。
  • 调用对象的getClass()方法。
  • 使用.class语法,例如String.class

一旦你有了Class对象,你就可以使用它来:

  • 获取类实现的接口。
  • 获取类的超类。
  • 访问类的构造函数、方法和字段。

例如,你可以使用getDeclaredMethods()来获取类中声明的所有方法,或者使用getField()来访问类的公共字段。
Java反射还可以用于:

  • 动态创建对象。
  • 动态调用方法。
  • 动态操作属性。
  1. 反射中重要的方法
  • 获取类的⽅法: forName
  • 实例化类对象的⽅法: newInstance
  • 获取函数的⽅法: getMethod
  • 执⾏函数的⽅法: invoke
  1. 如何理解反射?
    如何理解反射:平时我们要调用某个类中的方法的时候都需要创建该类的对象,通过对象去调用类里面的方法,反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了,在这种情况下(没有创建对象)我们都能够对它的方法和属性进行调用,我们把这种 动态获取对象信息和调用对象方法的功能称之为反射机制。

  2. 利用反射获取一个对象的步骤
    获取类的 Class 对象实例(这里有三种方法,后面会讲到)-> 根据 Class 对象实例获取 Constructor(构造函数) 对象 -> 使用 Constructor 对象的 newInstance 方法(实例)获取反射类对象

  3. 反射的常见利用
    用反射的地方一搬都是需要操作类的地方。这里先问下,反射机制的相关类在哪个包下?
    答案是java.lang.reflect.*,比如:

java.lang.reflect.Method 代表字节码中的方法字节码。代表类中的方法。
java.lang.reflect.Constructor 代表字节码中的构造方法字节码。代表类中的构造方法。
java.lang.reflect.Field 代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)。

那到底是怎么获取的呢?
动态加载类和调用方法: 反射机制可以在运行时加载类,并调用类的方法。这在编写插件系统、框架和库时非常有用,因为它允许应用程序根据需要动态加载和调用类的功能。

1
2
3
4
5
 // 动态加载类和调用方法
Class<?> clazz = Class.forName("com.example.MyClass");
Object instance = clazz.newInstance();
Method method = clazz.getMethod("myMethod", String.class);
method.invoke(instance, "parameter");

读取和修改类的字段: 反射机制使得可以在运行时获取和修改类的字段,即使这些字段是私有的。这在某些情况下(如序列化和反序列化)非常有用。

1
2
3
4
5
// 读取和修改类的字段
Field field = clazz.getDeclaredField("myField");
field.setAccessible(true); // 使私有字段可访问
Object value = field.get(instance); // 读取字段值
field.set(instance, newValue); // 修改字段值

构造对象: 反射机制可以使用类的构造器动态创建对象,甚至可以访问私有构造器。

1
2
3
4
   // 构造对象
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
constructor.setAccessible(true); // 使私有构造器可访问
Object instance = constructor.newInstance("parameter");
  1. 反射机制的优缺点
    反射机制在Java中提供了灵活性和通用性,使得程序能够在运行时动态地操作类、方法和属性,这增强了代码的灵活性和可扩展性,同时也支持与外部资源交互,为框架和库的设计提供了便利。然而,反射也存在性能开销高、安全性差、代码复杂度增加以及调试困难等缺点,需要谨慎使用以权衡其带来的好处和代价。

安全漫谈①-③

反射①

反射的第一篇文章主要讲了下 forName() 方法,这里简要介绍一下:
关于这个方法主要提出几个问题并且对其进行解决。

  1. 他的作用是什么?
  2. 使用方法是什么?
  3. 他和其他获取类的方法有什么区别?

首先第一个问题,直接说,Class.forName 方法的作用,就是初始化给定的类,通俗点说就是要求JVM查找并加载指定的类,也就是说JVM会执行该类的静态代码段,并返回与该类相关的Class对象。Class.forName(xxx.xx.xx)返回的是一个类。
至于他的使用方法,forName 有两个函数重载:

1
2
3
Class.forName(String name)

Class.forName(String name, boolean initialize, ClassLoader loader)

第⼀个就是我们最常⻅的获取class的⽅式,其实可以理解为第⼆种⽅式的⼀个封装:
默认情况下, forName 的第⼀个参数是类名;第⼆个参数表示是否初始化,这个参数为true时会加载执行静态函数的代码。;第三个参数就是 ClassLoader

注意:使用这个方法必须知道类的全路径名

这里ClassLoader是什么,我们先不做详细的介绍,ClassLoader翻译过来就是类加载器,简单的说一下他的作用就是,当你运行一个 Java 程序时,Java 虚拟机(JVM)会负责加载程序中用到的类。类加载器(ClassLoader)是负责在运行时查找和加载类文件的组件。在Java中,所有的类,都是由ClassLoader加载到JVM中执行的,但JVM中不止一种ClassLoader

那知道 forName() 方法的作用和使用方法之后,我们就来用下面一段代码实验一下
第一种加载:

1
2
3
4
5
6
7
8
9
10
public class Test {  
public static void main(String[] args)
throws ClassNotFoundException
{
//通过forName方法获取类的实例
Class c1 = Class.forName("java.lang.String");
System.out.print("Class represented by c1: "
+ c1.toString());
}
}

第二种加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
// 指定要加载的类名
String className = "java.lang.String";

// 指定是否立即初始化加载的类(true 表示立即初始化)
boolean initialize = true;

// 使用系统类加载器作为类加载器
ClassLoader loader = ClassLoader.getSystemClassLoader();

// 调用 Class.forName() 方法加载类
Class<?> clazz = Class.forName(className, initialize, loader);

// 输出加载的类名
System.out.println("Class represented by clazz: " + clazz);
}
}

第三个问题,要知道和其他方法的区别,那我们就得先知道还有哪些常用的方法
 1. 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过obj.getClass() 来获取它的类。调用Object类的getClass()方法来得到Class对象。比如:

1
2
3
4
5
6
7
8
9
10
public class Test {  
public static void main(String[] args) throws ClassNotFoundException {
// 假设使用 String 类来示例
String x = "Hello";
Class c1 = x.getClass();

System.out.print("Class represented by c1: "
+ c1.toString());
}
}

这个方法用的最少

  1. 使用Class类的中静态forName()方法获得与字符串相应的Class对象。就是我们前面说到的那个方法,也是最常用的一个方法。

  2. 获取Class类型对象的第三个方法最简单。用法就是:Test.class如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接拿它的 class 属性即可。这种方式虽然比较简单,但是需要导包,不然会编译错误,这一种方法不属于反射。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Test {  
    public class MyClass {
    // 一个简单的示例类
    }
    public static void main(String[] args) {
    // 使用 .class 属性获取 MyClass 类的 Class 对象
    Class<?> clazz = MyClass.class;

    // 打印 Class 对象
    System.out.println("Class represented by clazz: " + clazz);
    }
    }

这三种方法主要在使用场景上有所不同
调用对象的 getClass() 方法:
- 这种方式适用于已经存在对象实例,想要获取其对应的 Class 对象的情况。
- 通常用于在运行时动态获取对象的类型信息,例如对于传入的参数对象,需要了解其具体的类型。
使用 Class.forName() 方法:
- 这种方式适用于根据类名(字符串)动态地加载类,并获取其 Class 对象的情况。
- 通常用于实现插件机制、动态配置类名等场景。
直接使用 .class 属性:
- 这种方式是在编译时已知类名的情况下,直接获取该类的 Class 对象的一种简洁方式。
- 通常用于已知类名的情况下,或者在静态上下文中(例如静态方法或静态代码块)获取类的 Class 对象。

反射②

第二篇呢开头指出,在正常情况下,除了系统类,如果我们想拿到一个类,需要先 import 才能使用。而使用forName就不需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。

Java的普通类 C1 中支持编写内部类 C2 ,而在编译的时候,会生成两个文件: C1.class
C1$C2.class ,我们可以把他们看作两个无关的类,通过 Class.forName("C1$C2") 即可加载这个内部类。

获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方法class.newInstance() 的作用就是调用这个类的无参构造函数,这个比较好理解。不过,我们有时候
在写漏洞利用方法的时候,会发现使用 newInstance 总是不成功,这时候原因可能是:
① 你使用的类没有无参构造函数
② 你使用的类构造函数是私有的

最常见的情况就是 java.lang.Runtime ,这个类在我们构造命令执行Payload的时候很常见,但
我们不能直接这样来执行命令:

1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

会出现报错,原因是 Runtime 类的构造方法是私有的。
这里可能会问 为什么Runtime 类是私有的呢?为什么这其实涉及到很常见的设计模式:“单例模式”,也叫工厂模式。

比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连
接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取:

1
2
3
4
5
6
7
8
9
public class TrainDB {
private static TrainDB instance = new TrainDB();
public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}

这样,只有类初始化的时候会执行一次构造函数,后面只能通过 getInstance 获取这个对象,避免建
立多个数据库连接。

所以,Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime
象。那这里可能又有个问题,为什么Runtime.getRuntime()就可以获取到呢?

Runtime 类中,构造方法被私有化,这意味着无法直接通过 new 关键字来创建 Runtime 对象的实例。然而,Runtime 类提供了一个静态方法 getRuntime(),用于获取 Runtime 类的唯一实例。

我们将上述Payload进行修改即可正常执行命令了:

1
Class clazz = Class.forName("java.lang.Runtime");clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");

这里用到了 getMethodinvoke 方法。我们在开头也提到过。

getMethod 的作用是通过反射获取一个类的某个特定的公有方法。Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod 的时候,我们需要传给他你需要获取的函数的参数类型列表。

我们使用最简单的,也就是第一个,它只有一个参数,类型是String,所以我们使用
getMethod("exec", String.class) 来获取 Runtime.exec 方法。String.class 表示 String 类的 Class 对象,它指示了方法 exec 所接受的参数类型为 String

invoke 的作用是执行方法,它的第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是类对象
如果这个方法是一个静态方法,那么第一个参数是类

举个例子:

  1. 普通方法:普通方法是属于类的实例的方法,它们依赖于对象的存在。在调用普通方法时,第一个参数通常是该方法所属的类的实例(对象)。这个实例会作为调用方法的上下文,方法在执行时可以访问该实例的成员变量和其他方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Example {
    public void instanceMethod() {
    System.out.println("This is an instance method.");
    }

    public static void main(String[] args) throws Exception {
    Example obj = new Example();
    Method method = Example.class.getMethod("instanceMethod");
    method.invoke(obj); // 调用普通方法时,第一个参数是类对象
    }
    }

    在上面的例子中,instanceMethod 是一个普通方法,调用时需要传入一个 Example 类的实例作为第一个参数。

  2. 静态方法:静态方法是属于类本身的方法,它们不依赖于类的实例。在调用静态方法时,第一个参数是该方法所属的类对象本身。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Example {
    public static void staticMethod() {
    System.out.println("This is a static method.");
    }

    public static void main(String[] args) throws Exception {
    Method method = Example.class.getMethod("staticMethod");
    method.invoke(Example.class); // 调用静态方法时,第一个参数是类对象
    }
    }

    在上面的例子中,staticMethod 是一个静态方法,调用时直接使用 Example.class 作为第一个参数即可。

所以,总结一下:

  • 普通方法依赖于类的实例,需要使用实例对象来调用。
  • 静态方法不依赖于类的实例,可以直接使用类本身来调用。

所以我们正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是
method.invoke([1], [2], [3], [4]...)

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.reflect.Method;  

public class Test {
public void printMessage(String message) {
System.out.println("Normal method: " + message);
}

public static void main(String[] args) throws Exception {
Test obj = new Test();

// 正常方法调用
obj.printMessage("Hello, world!");

// 反射调用
Method method = Test.class.getMethod("printMessage", String.class);
method.invoke(obj, "Hello, world using reflection!");
}
}

所以我们将上面命令执行的payload分解一下就可以得到

1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");

反射③

在上面第二篇结束的时候留下了两个问题

  • 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
  • 如果一个方法或构造方法是私有方法,我们是否能执行它呢?

第一个问题,我们需要用到一个新的反射方法 getConstructor
getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。获取到构造函数后,我们使用 newInstance 来执行。
比如,我们常用的另一种执行命令的方式ProcessBuilder,我们使用反射来获取其构造函数,然后调用
start() 来执行命令:

1
Class clazz = Class.forName("java.lang.ProcessBuilder");((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

ProcessBuilder有两个构造函数:

1
2
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)

上面用到了第一个形式的构造函数,所以在 getConstructor 的时候传入的是 List.class

但是,我们看到,前面这个Payload用到了Java里的强制类型转换,将 newInstance 返回的 Object 类型转换为 ProcessBuilder 类型。因为 newInstance 返回的是一个泛型为 Object 的对象,而我们知道它实际上是一个 ProcessBuilder 对象,所以我们进行了强制类型转换。有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。

其实用的就是前面讲过的知识:

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

通过 getMethod("start") 获取到start方法,然后 invoke 执行,invoke 的第一个参数就是ProcessBuilder Object了。

运行一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Arrays;  
import java.util.List;

public class Test {
public static void main(String[] args) {
try {
// 反射获取 ProcessBuilder 类
Class clazz = Class.forName("java.lang.ProcessBuilder");

// 创建 ProcessBuilder 对象并启动进程
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));
} catch (Exception e) {
e.printStackTrace();
}
}
}

那么,如果我们要使用 public ProcessBuilder(String... command) 这个构造函数,需要怎样用反射执行呢?
对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。
所以,我们将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二种构造函数:

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)

在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给
ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:

1
2
3
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new
String[][]{{"calc.exe"}})).start();

那第二个问题,如果一个方法或构造方法是私有方法,我们是否能执行它呢?
这就涉及到 getDeclared 系列的反射了,与普通的 getMethodgetConstructor 区别是:
getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了。

getDeclaredMethod 的具体用法和 getMethod 类似, getDeclaredConstructor 的具体用法和
getConstructor 类似,我就不再赘述。

举个例子,前文说过Runtime这个类的构造函数是私有的,我们需要用 Runtime.getRuntime() 来获取对象。其实现在我们也可以直接用 getDeclaredConstructor 来获取这个私有的构造方法来实例化对象,进而执行命令,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.reflect.Constructor;  
import java.util.Arrays;
import java.util.List;

public class Test {
public static void main(String[] args) {
try {
// 反射获取 ProcessBuilder 类
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}

可见,这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用
setAccessible 修改它的作用域,否则仍然不能调用。

评论