java安全-反射

一、java安全 - 反射

文章是本人java安全漫谈系列文章学习过程的记录,希望通过写博客督促自己学习,如有错误希望各位大佬指正。

1、反射机制浅析

java反射机制是很多java漏洞的基础,尤其在反序列化中,反射十分重要,首先来了解一下反射是什么?

反射在很多语言中都存在,反射使对象能获取他的类,类可以通过反射获取方法,而获取方法后可以进行调用,在java中,反射可以赋予语言动态特性。动态特性是指相对于编译时将已编写代码进行生成对象,获取属性以及调用方法等操作,在程序运行时,根据反射的相关性质动态生成对象,获取属性以及调用方法等。

首先让我们看看反射的基础用法

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

这里涉及了反射过程中的一些重要方法:

  • forName:获取类
  • getMethod:获取类的方法
  • invoke:调用方法
  • newInstance:实例化对象

简单描述上述反射过程,forName通过className(类名)获取到一个特定类clazz,接下来Clazz中的getMethod方法通过methodName(方法名)获取需要执行的方法,最后通过newInstance方法对clazz进行实例化并通过invoke方法执行。

编写一个反射的方法类

public class invoke_exec {

    public void execute1(String className, String methodName) throws Exception {
        Class clazz = Class.forName(className);
        clazz.getMethod(methodName).invoke(clazz.newInstance());
    }
}

编写一个测试类

public class invokeTest {
    
    public void print() {
        System.out.println("反射成功");
    }
}

使用反射调用print方法

public class start {

    public static void main(String[] args) throws Exception {
        invoke_exec invokeTest = new invoke_exec();
        invokeTest.execute1("invokeTest", "print");
    }
}

2、forName方法分析

forName是获取class对象的一种方法,通过传入string类型参数,通过jvm查找和加载指定类,返回一个class对象的引用,此时对象没有被实例化。在反射中还需要通过其他方法实例化对象才能调用其中方法,如newInstance

forName有两种重载

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

分析源码发现forName方法都会返回forName0,而第一种重载中,initialize=trueloader是当前类的类加载器,在java中默认的ClassLoader通过类名加载类(类的完整路径)。

public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

这里提一下initialize初始化的问题,这里的初始化是指forName方法的静态初始化,并不会执行指定类的构造函数进行初始化,而是执行static代码块。这意味着恶意代码执行不需要通过invoke方法,仅仅通过forName方法对类进行静态初始化即可执行恶意代码。

这里可以编写恶意类,在static代码块中写入恶意代码

public class hack {

    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String commands = "calc.exe";
            Process p = rt.exec(commands);
            p.waitFor();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void print() {
        System.out.println("反射成功");
    }
}

用一个简单的方法进行验证

通过反射方法类获取hack类

public class start {

    public static void main(String[] args) throws Exception {
        invoke_exec invokeTest = new invoke_exec();
        invokeTest.execute1("hack", "print");
    }
}

3、对象实例化

在payload中经常使用java.lang.Runtime类进行构造,尝试使用上文的方法调用该类的方法进行命令执行。

这里先说明一下getMethod()invoke()方法的参数。getMethod()的第一个参数是需要调用的方法名,由于方法存在重载,如exec存在6个重载,采用getMethod("exec", String.class)获取只有一个String类型参数的exec重载。invoke()中第一个参数,若执行的方法是一个普通方法,传入类对象;若是一个静态方法,传入类。后续参数为调用方法需要传入的具体参数。

public class invoke_exec {

    public void execute2() throws Exception {
        Class clazz = Class.forName("java.lang.Runtime");
        clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "calc.exe");
    }
}
public class start {

    public static void main(String[] args) throws Exception {
        invoke_exec invokeTest = new invoke_exec();
        invokeTest.execute2();
    }
}

执行后可以发现报错了,发现无法通过private类型构造方法进行实例化。

在解决上述问题之前,首先了解一下反射中常用的两种实例化方法以及单例模式,经常使用到两种实例化方法:

  • Class.newInstance() 调用指定类的无参构造方法,且需要方法是public类型
  • Constructor.newInstance() 根据传入参数调用任意的构造方法,可以调用私有方法

也就是说,若指定类没有无参构造方法构造方法是私有的Class.newInstance()不能成功实例化。该类采用了单例模式,单例模式简单来说就是该类只允许在类初始化时执行一次构造函数产生一个实例,后续的功能只能通过特定方法来获取这一实例进行调用,如getInstance(),不能再实例化新的对象。

采用单例模式是为了保证一个类仅有一个实例,并提供一个全局访问点,这样可以解决一个全局使用的对象被频繁的创建和销毁。同时通过控制实例数量,节省资源;避免数据库增删改,文件处理等需要保证数据真实性的应用场景出现多实例同时操作导致的冲突。

跟进一下java.lang.Runtime类的源码,主要关注以下代码

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }
    
    private Runtime() {}
    }

Runtime类中,提供了一个private类型的无参构造方法,类内部通过该无参构造方法创建一个对象currentRuntime,提供一个public类型的静态方法getRuntime()通过返回currentRuntime使外部能够调用Runtime中的方法。

那么我们可以通过getRuntime()方法获取该类内部创建的实例代替newInstance()创建的实例进行调用,这样上述报错问题迎刃而解。

//正常执行方法
Runtime rt = Runtime.getRuntime();
rt.exec("calc.exe");

//反射执行方法
Class clazz = Class.forName("java.lang.Runtime");
Method exec = clazz.getMethod("exec", String.class);
Method getruntime = clazz.getMethod("getRuntime");
Object rt = getruntime.invoke(clazz);
exec.invoke(rt, "calc.exe");
//简化
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");

接下来进行一下测试

public static void main(String[] args) throws Exception {
        invoke_exec invokeTest = new invoke_exec();
        invokeTest.execute2();
    }
public class invoke_exec {

    public void execute2() throws Exception {
        Class clazz = Class.forName("java.lang.Runtime");
        clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");
    }

接下来还需要解决两个问题:

  • 指定类没有无参构造方法,且不存在单例模式的静态方法之类的获取对象方式,如何反射实例化?
  • 如何执行指定类的私有方法?

首先解决第一个问题,这里需要用到一个新的反射方法getConstructor,该方法的参数是一个参数数组,通过传递参数类型选定构造函数的某一个重载。首先拿hack类测试。

public void execute3() throws Exception {
        Class clazz = Class.forName("hack");
        clazz.getMethod("print").invoke(clazz.getConstructor().newInstance());
    }

接下来通过这种方式,实例化另一种命令执行的常用方式ProcessBuilder。首先简单介绍一下ProcessBuilder

该类用于创建操作系统进程,提供一种启动和管理进程的方法。在J2SE 1.5中新增,可以更好的管理Process类。每个ProcessBuilder实例管理一个进程属性集,他有一些特定的属性,如commandenvironmentworking directoryredirectErrorStream,通过start()方法,可以对特定属性的进程重复调用,方便创建多个相同的子进程。

ProcessBuilderProcess有较大的区别,ProcessBuilder是一个final类,不可被继承,而Process是一个抽象类,需要通过ProcessBuilder.start()或者Runtime.exec()进行实例化。ProcessBuilder相比Process,可以提供更好的管理进程,如设置工作目录,修改环境。

ProcessBuilder存在两种重载

public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)

第一个重载传入一个String类型元素的List,第二个重载传入可变数量参数。

使用getConstructorProcessBuilder实例化执行命令

第一种重载

public void execute4() throws Exception {
    Class clazz = Class.forName("java.lang.ProcessBuilder");
    //正常执行命令
    ((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();
	//反射执行命令
    clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));
}

第二种重载

public void execute5() throws Exception {
    Class clazz = Class.forName("java.lang.ProcessBuilder");
    //正常执行命令
    ((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();
    //反射执行命令
    clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));
}

第二个问题,如何执行私有方法

这里可以使用getDeclaredMethodgetDeclaredConstructor方法,他们与getMethodgetConstructor方法的区别在于

  • 不带Declared,获取当前类的公共方法以及从父类继承的方法。
  • Declared,获取当前类声明的方法,不获取从父类继承的方法。

这里修改hack类的无参构造方法和print方法为私有的,使用上述两个方法进行反射

public class hack {

    private hack() {}

    private void print() {
        System.out.println("反射成功");
        try {
            Runtime rt = Runtime.getRuntime();
            String commands = "calc.exe";
            Process p = rt.exec(commands);
            p.waitFor();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public void execute6() throws Exception {
        Class clazz = Class.forName("hack");
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Method m = clazz.getDeclaredMethod("print");
        m.setAccessible(true);
        m.invoke(constructor.newInstance());
    }

不调用getRuntime获取类内部创建的实例,使用getDeclaredConstructor直接实例化Runtime

public void execute7() throws Exception {
        Class clazz = Class.forName("java.lang.Runtime");
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        clazz.getMethod("exec", String.class).invoke(constructor.newInstance(), "calc.exe");
    }

接下来会继续更新

posted @ 2023-01-23 23:19  PIAOMIAO1  阅读(90)  评论(0编辑  收藏  举报