Java是一门编译型语言,经过编译器编译成字节码文件(.class),然后便可以运行在不同平台上的JVM虚拟机中。严格来说,Java 字节码(ByteCode)其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储在 .class 文件中。
双亲委派模型
双亲委派模型是 Java 类加载器 (ClassLoader)的一种工作机制,当一个类加载器收到加载类的请求时,它不会自己马上去加载,而是首先把这个请求委派给它的父类加载器去完成。只有当父类加载器无法加载(即在它的搜索路径下找不到)时,子类加载器才会尝试自己去加载。
这样设计的好处:避免重复加载,确保每个类在JVM中只被加载一次;防止恶意代码替换核心JAVA API,核心 API 总是由更高级别(更受信任)的类加载器加载。
先向上委派,再向下尝试。
1. 利用URLClassLoader加载远程class文件
Java的ClassLoader用于加载字节码文件,Java默认的ClassLoader根据类名来加载类,这个类名是类完整路径。URLClassLoader
是 Java 中一个非常实用的工具,它使得从文件系统、JAR 文件甚至远程 URL 动态加载类成为可能。它在其自身的 URL 路径中按顺序查找 .class
文件,从而实现了灵活的类加载功能。
Java根据配置项sun.boot.class.path
和java.class.path
中列举到的基础路径(这些路径是经过处理后的java.net.URL
类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
(新建一个TestCalc.java文件,命令javac TestCalc.java
编译成.class文件,将TestCalc.class文件移动到D:/vul/example目录下)
|
|
-
URL未以斜杠 / 结尾,则认为是一个JAR文件,使用
JarLoader
来寻找类,在Jar包中寻找.class文件 -
URL以斜杠 / 结尾,且协议名是
file
,则使用FileLoader
来寻找类,在本地文件系统中寻找.class文件
|
|
- URL以斜杠 / 结尾,且协议名不是
file
,则使用最基础的Loader
来寻找类。
|
|
协议不是file且以 / 结尾,会使用Loader寻找类,最常见的是http协议。可以通过这种方法直接加载远端的class文件,所以如果我们控制了目标Java ClassLoader的基础路径为一个http服务器,即可RCE。
2. 利用ClassLoader#defineClass直接加载字节码
无论是加载远程class文件,还是本地的class或jar文件,java都是通过以下方法进行:
|
|
loadClass
:首先检查请求的类是否已经被加载在JVM的类缓存中,如果类未被加载,它会调用其父类加载器的loadClass
方法来尝试加载。这个过程会递归向上,直到启动类加载器。如果父类加载器无法加载该类,当前ClassLoader
的findClass
方法才会被调用。findClass
:查找并加载类的字节码文件。具体实现如URLClassLoader
、AppClassLoader
等会在这里根据其配置的路径(如本地文件系统、JAR 包、远程 URL)去定位.class
文件。找到字节码后,它会将字节码读入字节数组。defineClass
:将字节码(以字节数组形式)转换成 JVM 内部的Class
对象,处理成真正的Java类。这是一个受保护的方法。
demo: base64部分是编译后Hello.class的内容,原始Hello.java就是打印Hello World。
|
|
解码 Base64 字符串得到 Hello.class
的字节码,通过反射调用系统类加载器的 defineClass
方法,将这个 Hello.class
加载到 JVM,通过反射调用 Hello
类的无参构造函数,创建 Hello
对象。
在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,不过它是常用攻击链TemplatesImpl的基础。
3. 利用TemplatesImpl加载字节码
看到在TemplatesImpl
类中还有一个内部类TransletClassLoader
,这个类是继承ClassLoader
,并且重写了defineClass
方法。
这里的
defineClass
由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。
向前追溯调用链:
|
|
追到最前面两个方法TemplatesImpl#getOutputProperties()
和TemplatesImpl#newTransformer()
,这两者的作用域是public,可以被外部调用。
TemplatesImpl
中对加载的字节码是有一定要求的:这个字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类。编写一个要调用的类HelloTemplatesImpl
。
|
|
简单POC:
base64部分就是对HelloTemplatesImpl.class
进行base64
这里定义了setFieldValue
方法,用来设置私有属性。设置了三个属性:_bytecodes
、_name
和_tfactory
。_bytecodes
是由字节码组成的数组;_name
可以是任意字符串,只要不为null即可;_tfactory
需要是一个TransformerFactoryImpl对象,因为 TemplatesImpl#defineTransletClasses()
方法里有调用到 _tfactory.getExternalExtensionsMap()
,如果是null会出错。
4. 利用BCEL ClassLoader加载字节码
Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。
BCEL提供了一个方便的API,可以进行分析、创建和操作Java Class文件。当你的 Java 源代码 (.java
文件) 被编译后,它会变成字节码 (.class
文件),这些字节码是 JVM(Java 虚拟机)能够理解和执行的指令。BCEL 允许你直接与这些字节码进行交互,而无需从头编写或修改 Java 源代码。BCEL 将 Class 文件中所有的符号信息,如方法、字段和字节码指令等,都表示为对象。
BCEL 提供两个类Repository
和Utility
:
Repository
用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译 java 文件生成字节码。Utility
用于将原生的字节码转换成BCEL格式的字节码。
示例demo:
首先创建恶意类TestCalc
|
|
encode
:将class文件转换成BCEL格式的字节码
注意导入的是com.sun.org.apache.bcel.internal.util.ClassLoader
decode
:BCEL ClassLoader 正是用于加载这串特殊的“字节码”。在前面加上 $$BCEL$$
是因为,BCEL 这个包中的类com.sun.org.apache.bcel.internal.util.ClassLoader
重写了 Java 内置的ClassLoader#loadClass()
方法。在 ClassLoader#loadClass() 中,其会判断类名是否是 $$BCEL$$
开头,如果是的话,将会对这个字符串进行 decode。
参考: