0x00 前言
最近打算学习Java代码审计,所以需要对Java语言中的一些特性有所熟悉,在各类的框架中Java反射经常被使用,在jsp一句话里面我们就可以利用反射获取Runtime中的exec方法来进行恶意代码执行,所以来学习一波
0x01 为什么会有反射?
在这之前我们先来了解一下JVM 和 java类加载的过程。
JVM
java之所以可以跨平台运行主要是因为java的JVM,JVM相当于是java的虚拟机(我们可以在windows上运行ubuntu等系统的虚拟机同故此jvm虚拟机同样可以跨平台运行),这个虚拟机用来运行我们的java代码。
类的加载
众所周知我们在运行java文件的时候都会经过一次编译,将我们的 .java 文件转换成我们的 .class 文件,然后通过我们的类加载器将 .class 的二进制数据加载到我们的内存中,具体流程如下图,这里我们主要来看我们运行时数据区中的方法区和堆区。
当 .class 文件被读入到内存中的时候,会将其放在运行时数据区中的方法区,同时在我们的堆区创建一个 java.lang.Class
对象,用来封装我们方法区中的数据结构同时向我们java程序员提供了访问方法区中数据结构中的接口。
所以当我们在运行代码的时候,其实就是将我们的代码给了JVM虚拟机,然后JVM虚拟机来负责运行,运行结束之后就结束了,程序也就停止了。
那么现在我们来回归正题,为什么会出现反射。
首先我们需要知晓 反射是为了增加java语言的动态性,如果正常的加载我们都需要通过new来对类进行实例化,然后才可以使用对象中的方法,但是在某些情况下这样会非常麻烦。
这是我们不使用反射时调用对象中方法的代码
page pg = new page("test");
pg.getName();
这么看起来是不是也蛮方便的?但是在某些情况下却不是这样的。
因为我们上面这样的加载方式有一个前提,那么就是必须要将我们的类进行实例化,但是我们如果要调用的类还没有被写好但是我们又想要调用怎么办呢?
举个例子:程序员A在写代码的时候需要用到程序员B的类,但是这时候程序员B的类还没有写好,那程序员A岂不是只能傻傻等着了吗?
所以这时候我们就需要引入我们的java反射,在程序员A写的时候利用反射来动态加载程序员B的类,这样的话就不会因为程序员B没有写好而耽误了
还有一种情况就是如果我们在项目中一开始使用的是Mysql来充当我们的数据库的,但是后面由于性能不符合不得不更换为Oracle的数据库,那么如果我们没有采用反射,那么我们是不是还得重新修改java的源代码,让JVM重新进行一个编译呢?
但是如果我们使用了反射,我们可以将我们的数据库驱动信息写入到我们的配置文件中,这样我们就可以通过直接修改配置文件来动态加载我们的数据库驱动了
举个例子:
下面是利用了反射的方法来动态加载我们的mysql驱动
Class.forName("com.mysql.jdbc.Driver");
但是如果我们要将我们的数据库驱动替换成我们的Oracle
我们只需要将括号中的内容进行一个替换就可以了
Class.forName("oracle.jdbc.OracleDriver");
在实际项目中我们只需要把括号里面的参数值换成从配置文件中获取即可,这样就可以通过修改配置文件来快速替换了
反射就是在运行的时候才知道要加载哪些类,然后可以在运行的过程中获取类的完整构造并且调用对应类中方法
0x02 反射
Java 反射机制在程序运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种 动态的获取信息 以及 动态调用对象的方法 的功能称为 java 的反射机制。
也就是我们在运行代码的时候动态加载类
在使用Java
反射机制时,主要步骤包括:
- 获取 目标类型的
Class
对象 - 通过
Class
对象分别获取Constructor
类对象、Method
类对象 &Field
类对象 - 通过
Constructor
类对象、Method
类对象 &Field
类对象分别获取类的构造函数、方法&属性的具体信息,并进行后续操作
接下来我们来看一下反射的例子
package com;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class Page {
private String name;
public Page(String name){
this.name = name;
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
class reftest{
public static void main(String[] args) throws Exception{
// 获取Page类的Class对象
Class clas = Class.forName("com.Page");
// 通过class对象来Constructor类对象也就是我们的构造器
Constructor constructor = clas.getDeclaredConstructor(String.class);
// 通过设置setAccessible为true可以访问到 私有的方法和属性
constructor.setAccessible(true);
// 通过构造器来实例化我们的Page对象,括号中的参数是我们构造函数的参数
Object object = constructor.newInstance("Test");
// 通过class对象来获取我们的Method类
Method getname = clas.getMethod("getName");
System.out.println(getname.invoke(object));
}
}
上面是一个例子,我们通过反射来调用Page类中的getName方法
反射方法使用主要的步骤如下:
- 通过路径获取类的对象实例
Class clas = Class.forName("com.Page");
- 根据Class对象获取对应的Constructor对象也就是我们的Constructor构造器
Constructor constructor = clas.getDeclaredConstructor(String.class);
- 使用Constructor 的 newInstance 来获取被实例化的类
Object object = constructor.newInstance("Test");
- 利用Method来获取类的方法进行方法的调用
Method getname = clas.getMethod("getName");
getname.invoke(object)
接下来我们来一块块的说明一下
获取类对象加载要被实例化的对象
通常有如下三种方法:
.getClass()
.class
Class.forName
// 获取 目标类型的`Class`对象的方式主要有4种
<-- 方式1:Object.getClass() -->
// Object类中的getClass()返回一个Class类型的实例
Boolean carson = true;
Class<?> classType = carson.getClass();
System.out.println(classType);
// 输出结果:class java.lang.Boolean
<-- 方式2:T.class 语法 -->
// T = 任意Java类型
Class<?> classType = Boolean.class;
System.out.println(classType);
// 输出结果:class java.lang.Boolean
// 注:Class对象表示的是一个类型,而这个类型未必一定是类
// 如,int不是类,但int.class是一个Class类型的对象
<-- 方式3:static method Class.forName -->
Class<?> classType = Class.forName("java.lang.Boolean");
// 使用时应提供异常处理器
System.out.println(classType);
// 输出结果:class java.lang.Boolean
我们通常使用的是第三种方法,前两种方法需要在相同的路径下才可以使用不然的话会引起冲突
获取类的构造函数
方法 | 说明 |
---|---|
getConstructor(Class…> parameterTypes) | 获得该类中与参数类型匹配的公有构造方法 | | getConstructors() | 获得该类的所有公有构造方法 | | getDeclaredConstructor(Class…> parameterTypes) |
获得该类中与参数类型匹配的构造方法(主要私有) |
getDeclaredConstructors() | 获得该类所有构造方法 |
通过Class对象获取的Constructor类对象来调用我们的有参构造函数
Constructor constructor = clas.getConstructor(String.class);
通过Class对象获取的Constructor类对象来调用我们的构造函数,默认为无参数的构造函数
Constructor constructor = clas.getConstructor();
通过构造类对象来创建我们Page类的对象
constructor.newInstance("Test");
通过上面几步我们就可以获取到对应类的对象了,接下来我们可以利用反射来对对象中的函数或者属性进行一个访问
利用反射获取类的方法
方法 | 说明 |
---|---|
getMethod(String name, Class…> parameterTypes) | 获得该类某个公有的方法 | | getMethods() | 获得该类所有公有的方法 | | getDeclaredMethod(String name, Class…> parameterTypes) |
获得该类某个方法(主要私有) |
getDeclaredMethods() | 获得该类所有方法 |
调用方法:
Method method = clas.getMethod("getName");
// 因为该方法无参数需求,所以我们就不需要传入参数
// 通过methd来调用我们的方法getName 需要传入实例
method.invoke(object)
调用有参数的函数
Method method = clas.getMethod("setName",String.class);
// 该方法有参数需求所以需要传入我们的参数
// 通过methd来调用我们的方法getName 需要传入实例和参数
method.invoke(object,"test")
利用反射获取类的属性
方法 | 说明 |
---|---|
getField(String name) | 获得某个公有的属性对象 |
getFields() | 获得所有公有的属性对象 |
getDeclaredField(String name) | 获得某个属性对 |
getDeclaredFields() | 获得所有属性对象 |
获取属性
Field field = clas.getDeclaredField("name");
// 如果目标属性是private 我们需要将setAccessible设为true 否则无法正常访问
field.setAccessible(true);
System.out.println((String) field.get(object));
修改属性
Field field = clas.getDeclaredField("name");
field.setAccessible(true);
// 将name属性值修改为change
field.set(object,"change");
System.out.println((String) field.get(object));
0x03 利用反射执行命令
由于Runtime类中含有exec方法所以我们可以利用反射来调用Runtime中的exec来进行命令执行
public class Loder {
public static void main(String[] args) throws Exception {
// 通过名字来获取对应的对象
Class clas = Class.forName("java.lang.Runtime");
// 我们这里利用构造来进行实例的初始化,首先来获取我们的构造函数
Constructor constructor = clas.getDeclaredConstructor();
// 由于Runtime的类的构造函数是私有的所以我们要设置状态来获取我们的私有状态的构造函数
constructor.setAccessible(true);
// 将我们的对象转化成我们的实例
Object object = constructor.newInstance();
// 获取我们类中的所有方法
Method method = clas.getMethod("exec", String.class);
// 来调用我们获取到的方法
method.invoke(object,"open -a Calculator");
}
}
由于为这里使用的是mac所以 打开计算机的命令为open -a Calculator,win操作系统只需修改为calc即可
成功弹出计算机
0x04 思考
在学习反射的过程中,想到了一个问题,既然对象的private是为了防止外部查看或篡改类的属性,但是反射的存在岂不是导致private就没有什么作用了吗?我们只需要通过setAttribute(true)就可以对参数进行修改查看
直到看到了如下的解答
Q:private修饰的方法可以通过反射访问,那么private的意义是什么?
A:Java的private修饰符不是为了绝对安全设计的,而是对用户常规使用Java的一种约束。就好比饭店厨房门口挂着“闲人免进”的牌子,但是你还是能够通过其他方法进去。
0x05 参考链接
https://m0nit0r.top/2020/06/07/java-deserialize-learn2/
https://blog.csdn.net/qq_40406704/article/details/98060936
https://juejin.im/post/6844903862575300622#heading-3
https://blog.csdn.net/u010164936/article/details/87922082
https://juejin.im/post/6844904005294882830
https://www.jianshu.com/p/356e1d7a9d11
木头师傅总结的很好,在单独看p神的文章时,感觉少了点什么东西。但是木头师傅这篇就比较有逻辑,受益了!
感谢师傅!后面也会争取写出更好的文章
加油啊师傅
哈哈哈好滴 一起加油
文章不错,对新人友好