Java安全之反射篇(1)

学习学习

Posted by lll-yz on November 18, 2021

反射篇(1)

学习自—>p神 Java安全漫谈

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

反射是大多数语言里都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过”反射”,我们可以将Java这种静态语言附加上动态特性。

PHP本身拥有很多动态特性,所以可以通过”一句话木马”来执行各种功能;Java虽然不像PHP那么灵活,但其提供的”反射”功能,也是可以提供一些动态特性。比如,这样一段代码,在你不知道传入的参数值的时候,你是不知道他的作用是什么的:

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

newInstance()和new()区别:

  1、两者创建对象的方式不同,前者是实用类的加载机制,后者则是直接创建一个类:

  2、newInstance创建类是这个类必须已经加载过且已经连接,new创建类是则不需要这个类加载过

  3、newInstance: 弱类型(GC是回收对象的限制条件很低,容易被回收)、低效率、只能调用无参构造,new 强类型(GC不会自动回收,只有所有的指向对象的引用被移除是才会被回收,若对象生命周期已经结束,但引用没有被移除,经常会出现内存溢出)

上面的例子中,我们演示了几个在反射中极为重要的方法:

  • 获取类的方法:forName
  • 实例化对象的方法:newInstance
  • 获取函数的方法:getMethod
  • 执行函数的方法:invoke

基本上,这几个方法包揽了Java安全里各种和反射有关的payload。

forName不是获取”类”的唯一途径,通常来说我们有如下三种方式获取一个”类”,也就是java.lang.Class对象:

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

在安全研究中,我们使用反射的一大目的,就是绕过某些沙盒。比如,上下文中如果只有Integer类型的数字,我们如何获取到可以执行命令的Runtime类呢?也许可以这样 (伪代码):

1.getClass().forName("java.lang.Runtime")

forName有两个函数重载:

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

第一个就是我们最常见的获取class的方式,其实可以理解为第二种方式的一个封装:

Class.forName(className)
//等于
Class.forName(className, true, currentLoader)

默认情况下,forName的第一个参数是类名;第二个参数表示是否初始化;第三个参数就是classLoader

ClassLoader是什么呢?它就是一个”加载器”,告诉Java虚拟机如何加载这个类。关于这一点,后面还有很多有趣的漏洞利用方法,之后讲到。Java默认的ClassLoader就是根据类名来加载类,这个类名是完整的路径,如java.lang.Runtime

第二个参数initialize(初始化)常常被人误解,其实在forName的时候,构造函数并不会执⾏,即使我们设置initialize=true ,那么这个初始化究竟指什么呢?

可以将这个”初始化”理解为类的初始化。让我们来看看这个例子:

public class Test {
    {
        System.out.printf("Empty block initial %s\n", this.getClass());
    }

    static {
        System.out.printf("Static initial %s\n", Test.class);
    }

    public Test() {
        System.out.printf("initial %s\n", this.getClass());
    }

    public static void main(String[] args) {
        Test test = new Test();
    }
}

Io5eM9.png

首先调用的是static {},其次是{},最后是构造函数。

其中,static {} 就是在”类初始化”的时候调用的,而{}中的代码会放在构造函数的super()后面,但在当前构造函数内容的前面。

所以说,forName中的initialize=true其实就是告诉Java虚拟机是否执行”类初始化”。

那么,假设我们有如下函数,其中函数的参数name可控:

public void ref(String name) throws Exception {
	Class.forName(name);
}

我们就可以编写一个恶意类,将恶意代码放在static {}中,从而执行:

//TouchFile.java

public class TouchFile {
    static {
        try {
            System.out.println("this is touch");
            
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"touch", "/tmp/success"};
            Process pc = rt.exec(commands);
            //rt.exec("calc");
            pc.waitFor();
        } catch (Exception e) {
            //do nothing
        }
    }
}
//Test02.java

public class Test02 {
    public void ref(String name) throws Exception{
        Class.forName(name);
    }

    public static void main(String[] args) throws Exception {
        Test02 test02 = new Test02();
        test02.ref("TouchFile");
    }
}

IoOwod.png

或调用计算机看看:

ITHlQO.png