文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox-Retired

HackTheBox-Active

VulnHub

代码审计

PHP代码审计

大数据安全

机器学习

基础学习

Python

Python基础

Python安全

Java

Java基础

Java安全

算法

Leetcode

随笔

经验

技术

 2021-03-11   2.1k

Java安全学习之反射

参考文章:

https://xz.aliyun.com/t/9117

https://xz.aliyun.com/t/7029

P神-java安全漫谈-反射机制1(知识星球-代码审计)
P神-java安全漫谈-反射机制2(知识星球-代码审计)
P神-java安全漫谈-反射机制3(知识星球-代码审计)

0x01 反射概念

反射的核心是JVM在运行状态的时候才动态加载类,对于任意一个类都能够知道这个类所有的属性和方法,并且对于任意一个对象,都能够调用它的方法/访问属性。这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。

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

0x02 java反射机制流程图

比如我们创建了一个类文件,经过javac编译之后,就会形成class文件,同时jvm内存会查找生成的class文件读入内存和经过ClassLoader加载,同时会自动创建生成一个Class对象,里面拥有其获取成员变量Field,成员方法Method和构造方法Constructor等方法。最后就是我们平时new创建对象。

反射机制本身就是获取一个类的Class对象,然后在用Class对象中的获取成员变量Field,成员方法Method和构造方法Constructor等方法,再去动态获取一个类或者调用一个类的属性,变量,构造方法等方式。

0x03 反射常见使用方法

1
2
3
4
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}

上⾯的例⼦中,演示了⼏个在反射⾥极为重要的⽅法:

  • 获取类的⽅法: forName

  • 实例化类对象的⽅法: newInstance

  • 获取函数的⽅法: getMethod

  • 执⾏函数的⽅法: invoke

基本上,这⼏个⽅法包揽了Java安全⾥各种和反射有关的Payload,首先来一个个的解释。

forName

获取”类“(java.lang.Class)的对象有三种方法:

  • obj.getClass() 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过obj.getClass() 来获取它的类;
  • Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接拿它的 class 属性即可。这个⽅法其实不属于反射。
  • Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取

forName有两个函数重载:

1
2
3
4
5
6
Class<?> forName(String name)
//name:class名称
Class<?> forName(String name, **boolean** initialize, ClassLoader loader)
//name:class名称
//initialize:是否进行“类初始化”
//loader:加载器

类初始化和类实例化的区别,先来看如下这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TrainPrint {
//初始块
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
//静态初始块
static {
System.out.printf("Static initial %s\n", TrainPrint.class);
}
//构造函数
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}
}

分别使用forName和实例化

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws IOException, ClassNotFoundException {
Class.forName("TrainPrint");
}
// Static initial class TrainPrint
public static void main(String[] args) throws IOException, ClassNotFoundException {
TrainPrint test= new TrainPrint();
}
// Static initial class TrainPrint
// Empty block initial class TrainPrint
// Initial class TrainPrint

类的实例化:静态初始块->初始块->构造函数
类的初始化:静态初始块

getMethod

getMethod 的作用是通过反射获取一个类的某个特定的公有方法。
而Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod 的时候,我们需要传给他你需要获取的函数的参数类型列表,如下:
Class.forName("java.lang.Runtime").getMethod("exec", String.class)

invoke

invoke方法位于Method类下,其的作用是传入参数,执行方法,它的第一个参数是执行method的对象:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类(之后会提到,这里其实不用那么死板,这个)
    它接下来的参数才是需要传入的参数。

以反射执行Runtime.getRuntime().exec("calc.exe");为例:

最开始的想法:

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

但是会执行报错,意思大概是Runtime的类对象不能通过newInstance()来获取对象(class.newInstance等于new class),是因为Runtime的类构造函数是一个private构造函数,只能通过getRuntime方法返回一个对象。

之所以构造函数是private属性,是因为Runtime遵循单例模式,不允许多次实例化,类初始化的时候会执行一次构造函数,只能通过 Runtime.getRuntime() 来获取到Runtime对象。

那么很自然的第二个想法:

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

分开来写会更清楚:

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);
exec = execMethod.invoke(runtime, "id");

以上我们就完成了通过类内置的静态方法获取类的实例,进一步调用一个public方法。
但是假如一个类没有无参构造方法(即不能class.newInstance()),也没有单例模式(只存在一个实例)的静态方法(即不能像getRuntime一样获取实例),那我们该如何实例化这个类呢?

指定的构造方法生成类的实例

假如我们这次使用ProcessBuilder来执行本地命令:

1
2
3
4
List<String> paramList = new ArrayList<>();
paramList.add("calc.exe");
ProcessBuilder pb = new ProcessBuilder(paramList);
pb.start();

可见,其构造函数是写入了一个字符串,不是无参构造方法,接下来我们会一步步进行转化。

getConstructor()函数可以选定指定接口格式的构造函数(由于构造函数也可以根据参数来进行重载),即:getConstructor(参数类型)

选定后我们可以通过newInstance(),并传入构造函数的参数执行构造函数,即newInstance(传入的构造函数参数)

ProcessBuilder有两个构造函数:

  • public ProcessBuilder(List<String> command)
  • public ProcessBuilder(String... command)(此处,String...这种语法表示String参数数量是可变的,与String[]一样)

分别执行构造方法获取实例的语句如下:

1
Class.forName("java.lang.ProcessBuilder").getConstructor("String.class").newInstance("id");
1
Class.forName("java.lang.ProcessBuilder").getConstructor("List.class").newInstance(Arrays.asList("id"));

执行完构造方法获取实例之后,其实可以通过类型强制转化,进而执行start()函数来执行命令。

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

但实际情况下并不一定可以这样利用,所以继续使用反射机制调用start函数,start函数不是一个静态函数,需要传入类的实例:

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

执行私有方法

以上都是方法或构造方法是public函数,用的都是getMethod、getConstructor,接下来需要使用getDeclaredMethod、getDeclaredConstructor:

  • getMethod等方法获取的是当前类中所有公共方法,包括从父类继承的方法
  • getDeclared等方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了

之前说到Runtime的构造方式是一个私有方法,从而不能直接调用,那么接下来我们来调用Runtime的构造方法来获取一个实例来执行命令:

1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Constructor cons = clazz.getDeclaredConstructor();
cons.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(cons.newInstance(), "id");

在获取到私有方法后,通过setAccessible(true)可以打破私有方法访问限制,从而进行调用。

待填坑…

Copyright © ca01h 2019-2021 | 本站总访问量