java反射机制


0x00 前言

今天开始,有时间就记录java安全的一些内容。

借用p神的一句话:Java安全可以从反序列化漏洞开始说起,反序列化漏洞⼜可以从反射开始说起。在很多java漏洞的利用方式中,我们都可以看到java反射的身影。

第一篇笔记,就从java反射开始记录。本篇需要的前置知识是面向对象的一些基本概念。

0x01 什么是java反射

java反射机制是一种间接操作目标对象的机制。在运行时,对于任意一个类,可以知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。

看到这个概念可能会感觉还是有些抽象,那先来讲一讲,java具体是怎么实现这个反射机制的。

0x02 java反射机制的实现

面向对象中我们知道,一个类中有成员变量,方法、构造方法等信息,而java的反射就是将java类中的各种成分映射成一个个的java对象。

Java中的java.lang下的Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。在反射包(java.lang.reflect)中,我们常用的类主要Constructor、Method、Field类。通过Class类和这三个类,可以将一个类的各个部分封装成其他对象,从而实现反射机制。

java.lang包以及java.lang.reflect包

java反射机制简单流程图

通过上面这个流程图简要说一下反射机制的流程。我们编写好一个User类的代码文件,然后用javac编译后形成User.class二进制文件,同时jvm内存会查找生成的class文件读入内存,再经过ClassLoader加载,会自动创建一个Class对象,里面拥有获取成员变量,成员方法和构造方法的方法。最后就是我们通过new来创建对象。

从中我们可以知道,在生成class文件的时候,也创建了Class对象,该对象用于表示我们所编写的类的类型信息。可以这么理解,Class类是所有类的类,而所有类是java.lang.Class类的实例对象,也就是class对象。所以反射机制本身不复杂,就是获取一个类的class对象,然后再通过class对象调用Class类的成员方法获取其成员变量Field实例,成员方法Method实例以及构造器Constructor实例。而在一些漏洞利用场景中,所用的exp往往就是这样获取到一个成员方法,然后传入对应参数来调用它从而达到最终目的。所以,在java安全中,我们着重掌握Class类和Method类即可。

我们下面就以这个User.java类为例,实现最终能够调用其中的成员函数效果。

package com.Anchor;


public class User{
    private String name;
    private Integer age;
    public User(){}
    public User(String name, Integer age){
        super();
        this.name = name;
        this.age = age;
    }
    public String getName(){
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "User [name=" + name + ", age=" + age + "]";
    }
}

通过java.lang.Class类获取class对象

上面说到,ClassLoader加载时(关于类加载器的内容下一篇讲),java虚拟机(jvm)会创建一个class对象,而获取一个class对象是反射中最关键的一个操作。在代码中获取class对象的方式的主要有三种:

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

全限定类名:就是类名全称,带包路径的用点隔开,例如本文中用的User类的全限定类名:com.Anchor.User。

其中相对常用或者说比较重要的就是forName()这个静态方法,我们可以在已知一些恶意类的类名的情况下,通过它来获取这些恶意类的class对象,下文还会提到这个forName方法。

package com.Anchor;

public class Main {
    public static void main(String[] args) throws Exception{
        //获取class实例的三种方式
        System.out.println("根据类名:  \t" + User.class);
        System.out.println("根据对象:  \t" + new User().getClass());
        System.out.println("根据全限定类名:\t" + Class.forName("com.Anchor.User"));
    }
}

输出结果:

根据类名:      class com.Anchor.User
根据对象:      class com.Anchor.User
根据全限定类名:    class com.Anchor.User

这里直接打印获得的class实例从而触发toString方法,不难看出Class类的toString方法默认返回的就是全限定类名。

获得这个class对象后,我们又可以调用以下几个常用的Class类中的成员方法:

class对象.getName() 获取全限定类名

class对象.getSimpleName() 获取类名

class对象.nweInstance()获取该类的实例化对象

package com.Anchor;

public class Main {
    public static void main(String[] args) throws Exception {
        Class userClass = Class.forName("com.Anchor.User");
        System.out.println("获取全限定类名:\t" + userClass.getName());
        System.out.println("获取类名:\t" + userClass.getSimpleName());
        System.out.println("实例化:\t" + userClass.newInstance());
    }
}

输出结果:

获取全限定类名:    com.Anchor.User
获取类名:    User
实例化:    User [name=null, age=0]

当然Class类中不止有这几个成员方法,还有一个关键的getMethod方法,下面我们将结合Method类,达到调用成员方法的效果。

通过java.lang.reflect.Method类获取成员方法

获取成员方法后调用方法,涉及两个动作,分别两用两个方法实现:

  1. Class类中的getMethod()方法

    返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法(注意,是公共成员方法,只能获取public方法)。

用法:

userClass.getMethod(要获取的成员方法名, 这个成员方法需要的参数类型)

  1. Method类中的invoke()方法。

用法:

method.invoke(调用该方法的对象, 该方法需要传入的参数值)

下面我们通过这两个方法实现将一个User实例的”name”属性的值设为”anchor”的操作:

package com.Anchor;

import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        Class userClass = Class.forName("com.Anchor.User"); //获得一个class实例
        Method method = userClass.getMethod("setName", String.class); // 通过class实例获得User类中的setName方法
        System.out.println("获得的Method对象为:" + method);
        method.invoke(userClass.newInstance(), "anch0r");// 实例化一个user对象并触发它的setName方法,传入的参数值为anch0r
    }
}

为了体现调用setName方法的效果我们将User类中的setName方法改造如下:

    public void setName(String name) {
        this.name = name;
        System.out.println(this.getName());
    }

输出结果:

获得的Method对象为:public void com.Anchor.User.setName(java.lang.String)
anch0r

可以看到name的值在运行过程中被设置为anch0r,代表setName方法调用成功。

Class类获取Method对象还有下面几个方法:

方法返回值 方法名称 方法说明
Method[] getMethods() 返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。
Method getDeclaredMethod(String name, Class<?>… parameterTypes) 返回一个指定参数的Method对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。
Method[] getDeclaredMethods() 返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。

0x03 java反射的价值

看到这里,也许你会疑惑,还是没有看到java反射的利用价值,光能调用成员函数有什么用?

那就来个命令执行的演示。我们来通过反射Runtime类实现命令执行。

上代码:

package com.Anchor;

import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        Class evilClass = Class.forName("java.lang.Runtime"); // 得到Runtime的class对象
        Method method = evilClass.getMethod("exec", String.class); // 从中获取exec方法的method实例
        Method runtimeMethod = evilClass.getMethod("getRuntime"); //从中获取getRuntime方法的method实例
        Object evil = runtimeMethod.invoke(evilClass); //调用getRuntime方法从而获得一个Runtime类对象
        method.invoke(evil, "calc.exe"); // 调用exec方法,运行exec命令,打开一个计算机
    }
}

核心是通过forName将Runtime类的class对象加载到内存中然后通过getMethod调用exec方法,进行命令执行。中途还需要通过getRuntime方法获得一个Runtime类对象。

成功执行命令,打开计算器

成功打开计算器

0x04 forName的两种重载

在平时,除了系统类,如果我们需要用一些类,除非有import导入,否则我们只能通过forName加载我们需要类的Class对象。

Class.java文件中显示,forName有两种重载方式,

第一种是用的最多的,参数只需要全限定类名即可

public static Class<?> forName(String className)

第一种forName重载方式

第二种

public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

第二种forName重载方式

第二种重载方式涉及另外两个参数,initialize和loader,loader需要传入一个ClassLoader类的对象,即类加载器,这个留到之后一篇文章详解,本篇暂且不谈。initialize要传入布尔值,代表是否初始化。

需要注意的是,第一种重载方式是第二种重载方式的一个特殊情况,即下面这两个forName是等价的:

Class.forName(className) //第一种重载方式
Class.forName(className, true, currentLoader)  //第一种相当于第二种重载方式选择初始化

那么这里的是否初始化到底代表什么?

想到初始化,第一个想到的就是构造函数,那就是会执行相应的构造函数?然后并非如此。

p神指出该初始化是告诉虚拟机是否执行”类的初始化“(类初始化是指类第一次加载到内存进程中要进行的操作)。

类在初始化的时候会调用其中的static{},即static块。那么我们可以通过将恶意代码放置在恶意类的static块中,通过forName加载该恶意类(选择第一种重载方式,默认“类初始化”),从而实现命令执行。

我们设计主函数如下,其中定义一个可以获取类的class对象的函数reflection

package com.Anchor;

public class Main {
    public static void reflection(String name) throws Exception {
        Class.forName(name);
    }

    public static void main(String[] args) throws Exception {
        reflection("com.Anchor.Evil");
    }
}

然后构造恶意类 Evil

package com.Anchor;

import java.lang.Runtime;
import java.lang.Process;

public class Evil {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String command = "calc.exe";
            Process pc = rt.exec(command);
            pc.waitFor();
        } catch (Exception e) {
        }
    }
}

执行Main函数,成功弹出计算器。

成功弹出计算器

假设reflection中的传入的参数可控,那么我们就可以像这样传入自己构造好的恶意类,达到命令执行效果。

0x05 小结

本篇只是初步从java反射角度了解java的一些底层原理,远没有涉及到实际的利用场景。其实利用好java反射机制,可以很好的绕过安全机制,这就涉及更深的内容了,本篇暂时不涉及,留到以后的篇幅去探索。

参考文章:

《java安全漫谈》

JAVA安全基础(二)– 反射机制

Java 基础 - 反射机制详解


文章作者: anch0r
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 anch0r !
  目录