Java代码审计之Java动态加载字节码

Java动态加载字节码

注:本文为复现文和学习文,原创极少,参考了大量参考链接里的内容~感兴趣者自行阅读原作

0x01.双亲委派模式

1.工作原理

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

2.loadClass

正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载

3.findClass

findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的

4.defineClass

defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象

ClassLoader类有如下核心方法:
loadClass(加载指定的Java类)
findClass(查找指定的Java类)
findLoadedClass(查找JVM已经加载过的类)
defineClass(定义一个Java类)
resolveClass(链接指定的Java类)

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

常用的类动态加载方式:

// 反射加载TestHelloWorld示例
Class.forName("com.anbai.sec.classloader.TestHelloWorld");
// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");

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

0x02.URLClassLoader

DemoHelloWorld.java

public class DemoHelloWorld {
    public String Hello() {
        System.out.println("Hello World.");
        return "invoke ok";
    }
}

DemoUrlClassLoad.java

package com.URLClassLoad;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class DemoUrlClassLoad {
    public static void main( String[] args ) throws Exception {
        URL[] urls = {new URL("http://127.0.0.1:8000/")};
        URLClassLoader loader = URLClassLoader.newInstance(urls);
        Class c = loader.loadClass("DemoHelloWorld");
        Object o = c.newInstance();
        //反射获取method方法
        Method method = o.getClass().getMethod("Hello");
        //反射去调用执行method方法
        String str = (String) method.invoke(o);
        System.out.println(str);
    }
}
public class DemoHelloWorld {
    public String Hello() {
        System.out.println("Hello World.");
        return "invoke ok";
    }
}

编译DemoHelloWorld.java

javac DemoHelloWorld.java

将编译好的DemoHelloWorld.class放置Web服务下

img

通过URLClassLoader成功的加载

img

0x03.defineClass

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

package com.DemoDefineClass;

public class DemoHelloWorld {
    public String Hello() {
        System.out.println("Hello World.");
        return "invoke ok";
    }
}

image-20211027203221858

javac DemoHelloWorld.java编译

DemoToBytes.java

package com.DemoDefineClass;
import sun.misc.IOUtils;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
public class DemoToBytes {
    public static void main(String[] args) throws IOException {
        InputStream fis = new FileInputStream("src/main/java/com/DemoDefineClass/DemoHelloWorld.class");
        byte[] bytes = IOUtils.readFully(fis, -1, false);
        System.out.println(Arrays.toString(bytes));
    }
}

得到DemoHelloWorld.class的字节码

img

DemoDefineClass.java

package com.DemoDefineClass;

import java.lang.reflect.Method;

class DemoDefineClass extends ClassLoader {

    // TestHelloWorld类名
    private static String testClassName = "com.DemoDefineClass.DemoHelloWorld";

    // TestHelloWorld类字节码
    private static byte[] testClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 60, 0, 31, 10, 0, 2, 0, 3, 7, 0, 4, 12, 0, 5, 0, 6, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 9, 0, 8, 0, 9, 7, 0, 10, 12, 0, 11, 0, 12, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 8, 0, 14, 1, 0, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 46, 10, 0, 16, 0, 17, 7, 0, 18, 12, 0, 19, 0, 20, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 8, 0, 22, 1, 0, 9, 105, 110, 118, 111, 107, 101, 32, 111, 107, 7, 0, 24, 1, 0, 14, 68, 101, 109, 111, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 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, 72, 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, 68, 101, 109, 111, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46, 106, 97, 118, 97, 0, 33, 0, 23, 0, 2, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 25, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 26, 0, 0, 0, 6, 0, 1, 0, 0, 0, 1, 0, 1, 0, 27, 0, 28, 0, 1, 0, 25, 0, 0, 0, 39, 0, 2, 0, 1, 0, 0, 0, 11, -78, 0, 7, 18, 13, -74, 0, 15, 18, 21, -80, 0, 0, 0, 1, 0, 26, 0, 0, 0, 10, 0, 2, 0, 0, 0, 3, 0, 8, 0, 4, 0, 1, 0, 29, 0, 0, 0, 2, 0, 30
    };

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        // 只处理TestHelloWorld类
        if (name.equals(testClassName)) {
            // 调用JVM的native方法定义TestHelloWorld类
            return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
        }

        return super.findClass(name);
    }

    public static void main(String[] args) {
        // 创建自定义的类加载器
        DemoDefineClass loader = new DemoDefineClass();

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

}

流程:loadClass过程中,因为双亲委派机制,在父类加载器找不到com.DemoDefineClass.DemoHelloWorld类时,最后通过自定义的findClass去加载,然后调用defineClass()方法生成类的Class对象

img

0x04.利用TemplatesImpl加载字节码

defineClass ,他决定了如何将一段字节流转变成一个Java类,Java默认的 ClassLoader#defineClass 是一个native方法

下面是简单的代码,来演示如何让系统的 defineClass 来直接加载字节码:

DemoHelloWorld.java

package com.DemoTemplatesImpl;

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 DemoHelloWorld extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers)
            throws TransletException {}
    public void transform(DOM document, DTMAxisIterator iterator,
                          SerializationHandler handler) throws TransletException {}
    public DemoHelloWorld() throws IOException {
        super();
        System.out.println("Hello World");
        Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
//        Runtime.getRuntime().exec("calc.exe");
    }
}

编译并base64编码

Javac DemoHelloWorld.java
cat DemoHelloWorld.class | base64

img

DemoTemplatesImpl.java

package com.DemoTemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import sun.misc.BASE64Decoder;
import java.lang.reflect.Field;
public class DemoTemplatesImpl {
    public static void main(String[] args) throws Exception {
        byte[] code =
                new BASE64Decoder().decodeBuffer("yv66vgAAADQALAoABgAdCQAeAB8IACAKACEAIgcAIwcAJAEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAmTGNvbS9EZW1vVGVtcGxhdGVzSW1wbC9EZW1vSGVsbG9Xb3JsZDsBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcAJQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAGPGluaXQ+AQADKClWAQAKU291cmNlRmlsZQEAE0RlbW9IZWxsb1dvcmxkLmphdmEMABkAGgcAJgwAJwAoAQALSGVsbG8gV29ybGQHACkMACoAKwEAJGNvbS9EZW1vVGVtcGxhdGVzSW1wbC9EZW1vSGVsbG9Xb3JsZAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAPwAAAAMAAAABsQAAAAIACgAAAAYAAQAAAAsACwAAACAAAwAAAAEADAANAAAAAAABAA4ADwABAAAAAQAQABEAAgASAAAABAABABMAAQAHABQAAgAJAAAASQAAAAQAAAABsQAAAAIACgAAAAYAAQAAAA0ACwAAACoABAAAAAEADAANAAAAAAABAA4ADwABAAAAAQAVABYAAgAAAAEAFwAYAAMAEgAAAAQAAQATAAEAGQAaAAEACQAAAD8AAgABAAAADSq3AAGyAAISA7YABLEAAAACAAoAAAAOAAMAAAAPAAQAEAAMABEACwAAAAwAAQAAAA0ADAANAAAAAQAbAAAAAgAc");
        TemplatesImpl obj = new TemplatesImpl();
        Class clazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        // 获取属性
        Field _name = clazz.getDeclaredField("_name");
        Field _bytecode = clazz.getDeclaredField("_bytecodes");
        Field _tfactory = clazz.getDeclaredField("_tfactory");
        // 设置属性可访问
        _name.setAccessible(true);
        _tfactory.setAccessible(true);
        _bytecode.setAccessible(true);
        // 设置属性的值
        _name.set(obj,"reader-9l");
        _bytecode.set(obj,new byte[][]{code});
        _tfactory.set(obj,new TransformerFactoryImpl());

        // TemplatesImpl#newTransformer()   TemplatesImpl#getTransletInstance()   TemplatesImpl#defineTransletClasses()   TransletClassLoader#defineClass()   ClassLoader#defineClass()
        obj.newTransformer();

        // getOutputProperties也能触发,因为里面的代码也是实现了newTransformer()
        // obj.getOutputProperties();
    }
}

image-20211027204635431

0x05 Gadgets 7u21 调试过程 - TemplatesImpl触发原理

TemplatesImpl#getOutputProperties也能触发,因为里面的代码也是实现了newTransformer()

image-20211028133450636

TemplatesImpl#getOutputProperties():↓

image-20211028133230341

也可以直接TemplatesImpl#newTransformer()

img

TemplatesImpl#newTransformer():↓

com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java

调用了TemplatesImpl#getTransletInstance()

img

TemplatesImpl#getTransletInstance():↓

com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java

因为要进入defineTransletClasses,那么_name属性的值不能为null,所以_name的值随便设置一个

同理_class属性的值要为null,所以不用设置就行。

image-20211028133432371

此时调用了TemplatesImpl#defineTransletClasses()

img

TemplatesImpl#defineTransletClasses():↓

com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java

_bytecode不能为null,否则就抛出了异常,至于赋予什么值,后面会分析

image-20211028140524831

红框里的代码:

TemplatesImpl类中的_tfactory变量需要有一个getExternalExtensionsMap方法,否则run方法就会报错从而程序中断,所以将_tfactory的值设置为TransformerFactoryImpl类。即_tfactory.set(obj,new TransformerFactoryImpl());

image-20211028140034876

这是没给_tfactory赋值

image-20211028141033976

运行报错了

image-20211028141816841

这里抛出一个疑问:

_tfactory赋值的类好像并不一定得要有getExternalExtensionsMap方法。同样能够触发~

image-20211028141858420

回归主题:接下来就进入到了loader.defineClass里

img

TransletClassLoader#defineClass():↓

com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java

img

ClassLoader#defineClass():↓

java/lang/ClassLoader.java

加载我们传递的字节码并生成对象。但是不会执行static代码。

img

接下来回到了TemplatesImpl#defineTransletClasses():↓

com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java

这里获取_bytecode转换后的类的父类,然后判断父类是不是AbstractTranslet类,只有是的话,才能继续下去。

image-20211028142344174

所以我们的恶意代码类必须要继承AbstractTranslet类

image-20211028142631015

最后漏洞触发代码就是红框里的这一行,_transletIndex是在defineTransletClasses()中判断是不是AbstractTranslet子类时赋值的,然后实例化了_bytecode的类,执行了静态方法或者构造函数里的代码。

image-20211028134542461

总结

  1. TemplatesImpl类的 _name 变量 != null,随便设置即可
  2. TemplatesImpl类的_class变量 == null
  3. TemplatesImpl类的 _bytecodes 变量 != null
  4. TemplatesImpl类的_bytecodes是我们代码执行的类的字节码。_bytecodes中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类
  5. 我们需要执行的恶意代码写在_bytecodes 变量对应的类的静态方法或构造方法中。
  6. TemplatesImpl类的_tfactory需要是一个拥有getExternalExtensionsMap()方法的类,使用jdk自带的TransformerFactoryImpl类

0x06.利用BCEL ClassLoader加载字节码

BCEL是一个用于分析、创建和操纵Java类文件的工具库

判断类名是否是$$BCEL$$开头,如果是的话,使用com.sun.org.apache.bcel.internal.classfile.Utility#decode将字符串解析成类字节码(带有攻击代码的恶意类),最后会调用defineClass注册解码后的类,一旦该类被加载就会触发类中的恶意代码。

DemoHelloWorld.java

package com.DemoBCEL;

public class DemoHelloWorld {
    public String Hello() {
        System.out.println("Hello World");
        return "String";
    }
}

DemoBCELBytes.java

package com.DemoBCEL;
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.Repository;

public class DemoBCELBytes {
    public static void main(String []args) throws Exception {
        // Repository 用于将一个Java Class先转换成原生字节码
        JavaClass cls = Repository.lookupClass(DemoHelloWorld.class);
        // Utility 用于将原生的字节码转换成BCEL格式的字节码
        String code = Utility.encode(cls.getBytes(), true);
        System.out.println(code);
    }
}

得到BCEL格式的字节码

image-20211027220644213

DemoBCEL.java

// 成功
package com.DemoBCEL;

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.lang.reflect.Method;

public class DemoBCEL {
    public static void main(String []args) throws Exception {
        Class testClass = new ClassLoader().loadClass("$$BCEL$$"+"$l$8b$I$A$A$A$A$A$A$A$7dQMO$c2$40$Q$7d$cbWK$5b$V$L$f8$85$ux$C$P$f6$e2$c5h$3c$88$g$P$8d$9a$60$f0$5c$60$83K$da$ae$v$c5$c4$7f$a5$k4$f1$e0$P$f0G$Z$a7$L$81$Y$8d$7b$98$d9yo$e6$ed$cc$ce$e7$d7$fb$H$80$7d$ec$Y$d0P$cc$a3$84$b2$8e$V$D$abX$d3$b1$aeaCC$85$nw$qB$R$l3$a4$h$cd$OC$a6$r$fb$9ca$c9$V$n$bf$i$H$5d$k$ddx$5d$9f$Q$db$95$3d$cf$efx$91H$e2$v$98$89$ef$c4$88$a1$ea$f6d$e0$9c$f2$40$9e$b4$ce$5cu$b9$e0$be$_oe$e4$f7$P$Z$b2$wb$u5$9a$ee$d0$7b$f0$i$df$L$HN$3b$8eD8$m$dah$cbq$d4$e3$e7$o$91$y$fe$ac$deK$f2$z$e8$c8k$d8$b4P$c5$W$83$a9$e8$9a$e25l$5b$a8$a1N$83L$f4$Y$w$ff4$c3P$987p$d5$j$f2$5e$fc$Dj$3f$8eb$k$d0g$c81$R$e5I$b7B$3a$d7$q$j$d3$D$dc$L$a8$e1$e2$l0$83v$9fD$7eHu$8d$dfc6$3b$a8$pG$9bHN$K$y$Z$89$acA$91C$9e$91$cf$ee$be$81$3d$x$da$q$9bS$60$g$WYk$92$80$F$y$92$a7$fd$a00$z$3ePb$80$f9$82$94$9d$7eE$c6$ce$3e$cd$q$M$f2I$99Nbs$Zs$s$b3$ac2$edo$j$7b$b1$e5$x$C$A$A");
        // 反射创建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);
    }
}

image-20211027220704503

0x07 参考链接

JDK反序列化Gadgets 7u21
    https://xz.aliyun.com/t/6884

双亲委派机制
    https://www.cnblogs.com/nice0e3/p/13719903.html/

   转载规则


《Java代码审计之Java动态加载字节码》 ske 采用 知识共享署名 4.0 国际许可协议 进行许可。