java反射机制2


0x00 前言

发现关于java反射还有很多内容需要补充,就接着java反射机制进一步记录有关反射的内容。

0x01 关于newInstance()方法

通过上次的java反射学习,我们知道,通过调用获得来的class对象的newInstance()方法可以实例化一个对象,通过该实例化的对象,我们就可以成功调用对应类中的成员方法。

但调用newInstance()方法是有条件的,前提是这个类有无参构造方法,并且构造方法是公有的,因为newInstance()作用就是调用类的公有的无参构造方法,进而实例化出一个对象。如果一个类没有公有的无参构造函数,那么调用newInstance()就会不成功。

拿我这篇文章中的java反射机制中的 “0x03 java反射价值”中的例子来说明。当时我是利用反射机制先获取java.lang.Runtime(这是我们命令执行构造payload最常用的类)的class对象,然后是获得exec()getRuntime()这两个成员方法,利用invoke方法先调用getRuntime()方法从而获得一个Runtime对象,然后才调用Runtime对象的exec()执行命令:

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类对象(这里invoke传入的参数也可以是null)
        method.invoke(evil, "calc.exe"); // 调用exec方法,运行exec命令,打开一个计算机
    }
}

看到这个过程,你可能会感觉到疑惑,为什么非要调用getRuntime()方法获得Runtime对象,而不考虑直接通过其中的evilClassnewInstance()来直接获得Runtime对象?

确实,上面这么做是显得有些麻烦,但是实践证明,通过newInstance()方法是不可行的,比如我们改成下面这行代码:

package com.Anchor;


public class Main {
    public static void main(String[] args) throws Exception {
        Class evilClass = Class.forName("java.lang.Runtime"); // 得到Runtime的class对象
        evilClass.getMethod("exec", String.class).invoke(evilClass.newInstance(), "calc.exe");
        //newInstance()实例化出一个Runtime对象,调用其中的exec方法
    }
}

这样是变简洁了,但是当运行这段代码时会产生一个报错:

报错

报错中有private这个字眼,我们看看rt.jar包下的java.lang.Runtime类的源码,可以发现其中的无参构造函数是私有方法,很明显,newInstance是不能调用这样的私有无参构造函数的,这就是为什么运行后会报错。

private Runtime() {}

Runtime的私有无参构造函数

不太了解java的人这时也许又会产生疑问,为什么会有私有的无参构造方法?真是不想让用户使用这个类才设计的吗?

其实并不是,这涉及到常见的设计模式:“单例模式”。

单例模式,说通俗点,就是想要保证一个类仅有一个实例,并且提供一个访问它的全局访问点,主要解决的问题就是程序中对一个类频繁的创造和销毁,导致效率下降。单例模式思路就是将构造函数设置成私有,然后设置一个静态方法来获取该构造方法,这样就不会频繁调用构造函数建立对象了,只需要一次借助对应的静态方法来调用构造函数创建单例。而我之前成功调用exec函数的方法就是用的这个静态方法来实现的。

Runtime类很明显就是一个单例模式设计的类,它其中有一个静态函数getRuntime()就是用来获取Runtime对象的。(在上一张图中我们可以看到getRuntime()函数代码)所以,就有这样的思路,利用getMethod方法拿到getRuntime的Method对象,通过调用它从而获得Runtime对象。

0x02 Constructor类

看到上面的例子后,疑问又产生了,如果有这样一个类,它没有无参构造方法(或者是无参构造方法私有),也没有单例模式中的静态方法,那么怎么通过反射机制来实现实例化操作呢?

解决上面这个问题,这里得引入反射中一个大类,Constructor。在上一篇java反射中我只提及了Class类和Method类,当时以为Constructor类并不是很重要,就把它给遗漏了,没想到它在安全中还是有作用的。

Constructor类存在于反射包(java.lang.reflect)中,反映的是Class 对象所表示的类的构造方法。在反射中,一个类的Constructor对象是可以通过Class类中的成员方法获得的,相关的方法如下:

方法返回值 方法名称 方法说明
static Class<?> forName(String className) 返回与带有给定字符串名的类或接口相关联的 Class 对象。
Constructor getConstructor(Class<?>… parameterTypes) 返回指定参数类型、具有public访问权限的构造函数对象
Constructor<?>[] getConstructors() 返回所有具有public访问权限的构造函数的Constructor对象数组
Constructor getDeclaredConstructor(Class<?>… parameterTypes) 返回指定参数类型、所有声明的(包括private)构造函数对象
Constructor<?>[] getDeclaredConstructors() 返回所有声明的(包括private)构造函数对象
T newInstance() 调用无参构造器创建此 Class 对象所表示的类的一个新实例。

常用的是其中的getConstructor方法,和getMethod方法类似getConstructor接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。通过getConstructor这些方法,我们不仅可以解决上面没有无参构造函数的问题,还可以任意调用自己需要的那个构造函数。

我们先以上次的User类为例尝试调用其中的构造函数,一共是三个调用函数,分别是无参、只有一个参数以及有两个参数的构造函数,其中只有一个参数的构造函数是私有的。

package com.Anchor;


public class User {
    private String name;
    private Integer age;

    public User() {
    }

    private User(String name) {
        this.name = name;
    }

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User [name=" + name + ", age=" + age + "]";
    }
}

测试的Main函数如下:

package com.Anchor;


import java.lang.reflect.Constructor;

public class Main {
    public static void main(String[] args) throws Exception {
        Class userClass = Class.forName("com.Anchor.User");
        User user0 = (User) userClass.newInstance(); //通过Class对象的newInstance方法调用无参构造函数
        Constructor cs1 = userClass.getConstructor(); //通过getConstructor获得无参构造函数的Constructor对象
        User user1 = (User) cs1.newInstance(); //第一种是newInstance
        System.out.println("Class对象的newInstance方法:\t" + user0);
        System.out.println("getConstructor[no parameters]:\t" + user1);
        System.out.println("--------------------------------------------");

        // 得到只有一个参数的构造函数的Constructor对象,使用getDeclaredConstructor方法,可以获得私有方法的Constructor对象
        Constructor cs2 = userClass.getDeclaredConstructor(String.class);
        cs2.setAccessible(true); //由于是private必须设置可访问
        User user2 = (User) cs2.newInstance("anch0r");
        System.out.println("getDeclaredConstructor[one parameter]:\t" + user2);

        System.out.println("--------------------------------------------");


        Constructor cs3 = userClass.getConstructor(String.class, Integer.class); //得到有两个参数的构造函数的Constructor对象
        User user3 = (User) cs3.newInstance("anch0r2", 100);
        System.out.println("getConstructor[two parameters]:\t" + user3);
    }
}

最后运行结果为:

Class对象的newInstance方法:    User [name=null, age=null]
getConstructor[no parameters]:    User [name=null, age=null]
--------------------------------------------
getDeclaredConstructor[one parameter]:    User [name=anch0r, age=null]
--------------------------------------------
getConstructor[two parameters]:    User [name=anch0r2, age=100]

接下来以两个例子来说明Constructor类和相关的一些方法引入的意义。

设置setAccessible(true)绕过访问权限

你也许注意到上面这个例子中的一个方法:setAccessible(true)

在程序中,调用这一方法,可以取消java语言访问控制的检查能力。我们通过getDeclaredConstructor获得一个私有构造函数的Method对象后,可以用setAccessible(true)来实现绕过私有权限的访问控制。而从上文我们知道,就是因为java.lang.Runtime类中的无参构造函数是私有的我们才没法通过Class对象的newInstance方法来实例化对象,现在我们有了一个可以绕过private的方式了,那么就来利用setAccessible(true)改造之前的命令执行代码:

package com.Anchor;


import java.lang.reflect.Constructor;

public class Main {
    public static void main(String[] args) throws Exception {
        Class evilClass = Class.forName("java.lang.Runtime");//老规矩,获取一个class对象
        Constructor cs = evilClass.getDeclaredConstructor(); //利用getDeclaredConstructor获取私有的无参构造函数的Constructor对象
        cs.setAccessible(true); // 取消java的访问控制的检查机制,绕过访问控制
        evilClass.getMethod("exec", String.class).invoke(cs.newInstance(), "calc.exe");
        // 通过无参构造函数的Constructor对象实例化,再借助getMethod获取exec的Method类,最后调用exec函数。
    }
}

运行后成功弹出计算器:

成功弹出计算器

调用java.lang.ProcessBuilder不同类型的构造函数实现命令执行

在构造命令执行的payload时,除了Runtime类,ProcessBuilder类也是比较常用的,一般使用反射来获取其构造函数,然后调用其中的start() 方法来执行命令。

我们先通过查看源码研究一下这个类的的构造函数:

ProcessBuilder类两种构造方法

可以看到该类构造函数有两个重载方式。

第一个重载

第一个重载需要传入的参数类型是列表类型List:

public ProcessBuilder(List<String> command) {
        if (command == null)
            throw new NullPointerException();
        this.command = command;
    }

我们在使用getConstructor获得Constructor对象时,需要将传参类型修改为List.class,所以我们能够立刻写出下面这个payload:

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

这里用Arrays.asList方法将字符串"calc.exe"转为了列表类型再传入到Constructor类的newInstance方法中,从而执行构造函数public ProcessBuilder(List<String> command)

这里虽然执行成功,但是有个缺点,使用了强制类型转换(也就是第二行前面的(ProcessBuilder)语法),然而在一般情况下,表达式上下文是没有这种语法的。我们仍然需要用java反射改进这一步,避免使用强制类型转换,如下面这行代码:

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

这里通过getMethod("start") 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是ProcessBuilder Object了,就不需要再进行强制类型转换了。

运行成功弹出计算器:

成功弹出计算器

第二个重载

第二个重载需要传入的参数类型比较特殊,是可变长的参数

    public ProcessBuilder(String... command) {
        this.command = new ArrayList<>(command.length);
        for (String arg : command)
            this.command.add(arg);
    }b

java可变长参数:

java一般使用以使用 ... 这样的语法来表示这个函数的参数个数是可变的,在编译时java会将可变长参数编译成一个数组类型,可以说下面这两种形式是等价的:

public void hello(String[] names) {}

public void hello(String...names) {}

对于hello这个函数,我们就可以传入一个字符串数组类型的数据,比如String[] names = {"hello", "world"};

如果我们要实现对这第二个构造函数的重载,就要给它传入可变长参数。我们可以直接将可变长参数当成数组,也就是说将字符串数组类型的class对象:String[].class 传给 getConstructor作为参数类型 ,从而获取 ProcessBuilder 类这第二种重载的构造函数。

具体代码如下:

Class evilClass = Class.forName("java.lang.ProcessBuilder");
evilClass.getMethod("start").invoke(evilClass.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));

运行后成功弹出计算器:

成功弹出计算器

0x03 小结

这一篇主要是解决上一篇的一些疑惑,同时在引入了Constructor类概念后加入了一些新的玩法。

参考文章:

《java安全漫谈》

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


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