java代码审计自学:反射机制

0x00 简介

主要是因为自己的学习Java 代码审计中的学习思路吧,主要自己一个人学习,有点闭门造车,百度学习法,但是还是记录一下,也分享一下,也便于将来的总结和反思,如果我能终能学到什么,我也会重新梳理思路,为那些自学者提供一个好的思路,所以有了下面的系列文章java代码审计自学篇。

P牛的文章中说到:

Java安全可以从反序列化漏洞开始说起,反序列化漏洞⼜可以从反射开始说起。

0x01 Java反射机制

我之前觉得Java学起来感觉是比较死的,因为我只是站在变成的角度,php的各种动态的调用,免杀起来都方便的不行,但是发现java的提供的“反射”功能,也是可以提供⼀些动态特性,也是灵活的

所以,Java反射(Reflection)是Java非常重要的动态特性

我们通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息

还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。

简单的说就是,我们用对象可以通过反射获取他的类,用类可以通过拿到它的所有⽅法(包括私有),拿到的⽅法可以为所欲为??

0x02 基础知识

⼏个在反射⾥极为重要的⽅法:

1.获取class的字节码对象

Java反射操作的是java.lang.Class对象,所以我们需要先想办法获取到Class对象,通常我们有如下几种方式获取一个类的Class对象:

  1. 类名.class,如:com.zeo.sec.Test.class。如果你已经加载了某个类,那么就直接拿它的 class 属性

  2. Class.forName("com.zeo.sec.Test.class")。如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取

  3. classLoader.loadClass("com.zeo.sec.Test.class");

  4. obj.getClass()如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过obj.getClass() 来获取它的类

以最常用的代码执行获取Runtime类Class对象代码片段:

1
2
3
4
String className     = "java.lang.Runtime";
Class runtimeClass1 = Class.forName(className);
Class runtimeClass2 = java.lang.Runtime.class;
Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);

2.获取名字

可以反射类名。

1
2
3
getName()//获取全名 例如:com.test.Demo

getSimpleName()//获取类名 例如:Demo

3.获取构造函数

1
2
3
4
5
6
7
getConstructors()//获取所有公开的构造函数

getConstructor(参数类型)//获取单个公开的构造函数

getDeclaredConstructors()//获取所有构造函数

getDeclaredConstructor(参数类型)//获取一个所有的构造函数

获取当前类所有的成员方法:

1
Method[] methods = clazz.getDeclaredMethods()

获取当前类指定的成员方法:

1
2
Method method = clazz.getDeclaredMethod("方法名");
Method method = clazz.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);

4.实例化类对象的⽅法: newInstance()

class.newInstance() 的作用就是调用这个类的无参构造函数,如果使用 newInstance 总是不成功,这时候原因可能是:

  1. 你使用的类没有无参构造函数 (getConstructor方法解决)

  2. 你使用的类构造函数是私有的 (getDeclaredConstructor方法解决)

5.反射调用方法执⾏函数的⽅法: invoke

获取到java.lang.reflect.Method对象以后我们可以通过Methodinvoke方法来调用类方法。

调用类方法代码片段:

1
method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);
1
2
3
invoke 的作用是执行方法,它的第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是 类的实例对象
如果这个方法是一个静态方法,那么第一个参数是 类

0x03 特殊情况

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

使用新的反射方法 getConstructor

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

如果一个方法或构造方法是私有方法,我们是否能执行它呢

普通的 getMethod 、getDeclaredMethod 区别是:
getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了

getDeclaredConstructor和getConstructor都可以获取到类构造方法,区别在于:

getConstructor无法获取到私有方法,所以一般在获取某个类的构造方法时候我们会使用getDeclaredConstructor去获取构造方法。如果构造方法有一个或多个参数的情况下我们应该在获取构造方法时候传入对应的参数类型数组,如:clazz.getDeclaredConstructor(String.class, String.class)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取Runtime类对象
Class runtimeClass1 = Class.forName("java.lang.Runtime");

// 获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);

// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();

// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);

// 调用exec方法,等价于 rt.exec(cmd);
Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);

// 获取命令执行结果
InputStream in = process.getInputStream();

// 输出命令执行结果
System.out.println(IOUtils.toString(in, "UTF-8"));

反射调用Runtime实现本地命令执行的流程如下:

  1. 反射获取Runtime类对象(Class.forName("java.lang.Runtime"))。
  2. 使用Runtime类的Class对象获取Runtime类的无参数构造方法(getDeclaredConstructor()),因为Runtime的构造方法是private的我们无法直接调用,所以我们需要通过反射去修改方法的访问权限(constructor.setAccessible(true))。
  3. 获取Runtime类的exec(String)方法(runtimeClass1.getMethod("exec", String.class);)。
  4. 调用exec(String)方法(runtimeMethod.invoke(runtimeInstance, cmd))。

参考

P牛知识星球中的java漫谈

凌天实验室:https://mp.weixin.qq.com/s/SA\_M0yQiCh8nM0qL2xwT-A