JAVA加载字节码

JAVA–加载字节码

前置

定义:

Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储 在.class文件中。

众所周知,不同平台、不同CPU的计算机指令有差异,但因为Java是一门跨平台的编译型语言,所以这 些差异对于上层开发者来说是透明的,上层开发者只需要将自己的代码编译一次,即可运行在不同平台 的JVM虚拟机中。只要你的编译器能够将代码编译成.class文件,都可以在JVM虚拟机中运行(甚至可以用Scala、Kotlin这样的语言编写代码)

image-20240531114228815

但是后文想说的字节码是广义”字节码”而不是狭义的“Java字节 码”:所有能够恢复成一个类并在JVM虚拟机里加 载的字节序列,都在我们的探讨范围内

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

利用URLClassLoader加载远程class文件

package com.govuln;

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

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

代码如上

我们来具体分析一下

Java的ClassLoader来用来加载字节码文件最基础的方法

在反射那里有提到过

这里再补充一下:

ClassLoader是Java中一个负责动态加载类的抽象类。它是Java应用程序动态扩展其功能的关键机制。当Java虚拟机(JVM)需要某个类时,它会调用适当的ClassLoader实例来加载这个类的字节码文件(.class文件)到内存中,并创建对应的Class对象,之后该类就可以被实例化和使用了

这里要说的是:

ClassLoader.SecureClassLoader.URLClassLoader

他们三个是继承关系

URLClassLoader扩展了类加载的能力,使得可以从一个或多个指定的URL(统一资源定位符)中加载类和资源。这意味着,除了类路径(classpath)上的资源,URLClassLoader还可以从网络、文件系统中的特定位置、jar文件等加载类,提供了更为灵活的类加载方式。

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

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

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

我们正常开发的时候通常遇到的是前两者,那什么时候才会出现使用 Loader 寻找类的情况呢?当然是非 file 协议的情况下,最常见的就是 http 协议

而 刚刚我给出的代码就是用来测试Java是否能从远程HTTP服务器上加载.class文件(用Loader寻找类)

image-20240531132210078

成功请求到我们的 /Hello.class 文件,并执行了文件里的字节码,输出了”Hello World”。

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

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

首先我们要知道:不管是加载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用

image-20240531221616983

理解:

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

所以真正核心的部分其实是 defineClass–>它决定了如何将一段字节流转变成一个Java类

Java 默认的 ClassLoader.defineClass 是一个native方法,逻辑在JVM的C语言代码中

写一段代码让系统的 defineClass 来直接加载字节码:

package com.govuln;

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

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

byte[] code = Base64.getDecoder().decode(
"yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");

Class hello = (Class) defineClassMethod.invoke(
ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
helloClass.newInstance();
}
}
"yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEA
Bjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVs
bG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZh
L2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry
ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n
OylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoA
AAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM
解码:
是一个Java字节码
定义了一个名为Hello的类,其中包含一个默认构造函数和一个main方法。main方法内仅有一行代码,用于向标准输出打印字符串Hello World。这是学习Java字节码或动态加载类时常会见到的示例。

在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造函数,初始化代码才能被执行

并且,即使我们将初始化代码放在类的static块中,在 defineClass 时也无法被直接调用到。

所以,如果我们要使用 defineClass 在目标机器上执行任意代码,需要想办法调用构造函数

image-20240531223835960

以上代码输出了hello world

这里,因为系统的 ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问,不得 不使用反射的形式来调用。

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

利用TemplatesImpl加载字节码

如上面所说

虽然大部分上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它(否则他 也没存在的价值了对吧),这就是 TemplatesImpl

我们看下源码

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

image-20240531225005470

这个类里重写了 defineClass 方法,并且这里没有显式地声明其定义域。

Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为default

所以也就是说这里的 defineClass 由其父类的 protected类型变成了一个default类型的方法,可以被类外部调用

我们从 TransletClassLoader.defineClass() 向前追溯一下调用链:**(ctrl+alt+H)**

TemplatesImpl.getOutputProperties() -> TemplatesImpl.newTransformer() ->
TemplatesImpl.getTransletInstance() -> TemplatesImpl.defineTransletClasses()
-> TransletClassLoader.defineClass()

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

public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
byte[] code = Base64.getDecoder().decode(
"yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwAAQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwADwABABAAAAACABE=""
);

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] { code });
setFieldValue(obj, "_name", "HelloTemplatesImpl");
TransformerFactoryImpl tfactory = new TransformerFactoryImpl();
setFieldValue(obj, "_tfactory", tfactory);

obj.newTransformer();
}

其中, setFieldValue 方法用来设置私有属性,可见,这里我设置了三个属性: _bytecodes_name_tfactory

  • _bytecodes 是由字节码组成的数组;

  • _name 可以是任意字符串,只要不为null即可;

  • _tfactory 需要是一个 TransformerFactoryImpl 对象,因为TemplatesImpl#defineTransletClasses() 方法里有调用到_tfactory.getExternalExtensionsMap() ,如果是null会出错。

base64解码也是字节码文件的编码

另外,值得注意的是,TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet子类

所以 这里我们写一个特殊的类

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 HelloTemplatesImpl extends AbstractTranslet {

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

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

public HelloTemplatesImpl() {
super();
System.out.println("Hello TemplatesImpl");
}
}

它继承了 AbstractTranslet 类,并在构造函数里插入Hello的输出。将其编译成字节码,即可被 TemplatesImpl 执行了:

image-20240531231840577

利用BCEL ClassLoader加载字节码

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

详解

我们可以通过BCEL提供的两个类 RepositoryUtility 来利用:

  • Repository 用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码;
  • Utility 用于将原生的字节码转换成BCEL格式的字节码:
package com.govuln;

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 HelloBCEL {

public static void main(String[] args) throws Exception {
JavaClass cls = Repository.lookupClass(evil.Hello.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
}
}

image-20240602163631813

而BCEL ClassLoader用于加载这串特殊的“字节码”,并可以执行其中的代码:

image-20240602163658570

BCEL ClassLoader在Fastjson等漏洞的利用链构造时都有被用到,其实这个类和前面的 TemplatesImpl 都出自于同一个第三方库,Apache Xalan

在Java 8u251的更新中,这个ClassLoader被移除了,所以之后只能且用且珍惜了