参考文章

前言

BCEL平常在测试反序列化的时候也经常会用到,比如延时测Gadget以及在某些场景下执行命令不是那么顺手的情况下选择BCEL去打内存马,就像Fastjson和Thymeleaf SSTI这种。以前也只是用到这个BCEL但是没有仔细学习过,下面简单学习记录下BCEL。

Java安全之BCEL ClassLoader

目录

写在前面#

BCEL平常在测试反序列化的时候也经常会用到,比如延时测Gadget以及在某些场景下执行命令不是那么顺手的情况下选择BCEL去打内存马,就像Fastjson和Thymeleaf SSTI这种。以前也只是用到这个BCEL但是没有仔细学习过,下面简单学习记录下BCEL。

About BCEL

BCEL Classloader在 JDK < 8u251之前是在rt.jar里面。
同时在Tomcat中也会存在相关的依赖
tomcat7

org.apache.tomcat.dbcp.dbcp.BasicDataSource

tomcat8及其以后

org.apache.tomcat.dbcp.dbcp2.BasicDataSource

而在rt.jar!/com/sun/org/apache/bcel/internal/util/包下,有Classloader这么一个类,可以实现加载字节码并初始化一个类的功能,该类也是个Classloader(继承了原生的Classloader类)重写了loadClass()方法,源码如下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
Class cl = null;

/* First try: lookup hash table.
*/
if((cl=(Class)classes.get(class_name)) == null) {
/* Second try: Load system class using system class loader. You better
* don't mess around with them.
*/
for(int i=0; i < ignored_packages.length; i++) {
if(class_name.startsWith(ignored_packages[i])) {
cl = deferTo.loadClass(class_name);
break;
}
}

if(cl == null) {
JavaClass clazz = null;

/* Third try: Special request?
*/
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);
else { // Fourth try: Load classes via repository
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
}
else
throw new ClassNotFoundException(class_name);
}

if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
}

if(resolve)
resolveClass(cl);
}

classes.put(class_name, cl);

return cl;
}

首先会判断类名是否以$$$BCEL$$$开头,之后调用createClass()方法拿到一个JavaClass对象最终通过defineClass()加载字节码还原类。

调试分析

先来看下简单的使用,在同一包下,准备一个恶意类

1
2
3
4
5
6
7
8
9
10
11
12
13
package MemoryShell.BCEL;

import java.io.IOException;

public class calc {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}

准备一个BCEL的demo,运行即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package MemoryShell.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;
import com.sun.org.apache.bcel.internal.util.ClassLoader;


public class BCELDemo {
public static void main(String[] args) throws Exception {

JavaClass cls = Repository.lookupClass(calc.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);

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

image-20230425210508013

这样就成功弹其计算器了

进入loadClass(),首先会判断类名是否以$$$BCEL$$$开头,之后调用createClass()方法拿到一个JavaClass对象最终通过defineClass()加载字节码还原类。

这里原因就是刚开始对loadClass的分析

那么我们就进行断点调试跟一下

image-20230425211759344

这里由于我们前面给文件头加了个BCEl字节码,于是这里就可以进入createClass()

createClass源码

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
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);

JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

clazz = parser.parse();
} catch(Throwable e) {
e.printStackTrace();
return null;
}

// Adapt the class name to the passed value
ConstantPool cp = clazz.getConstantPool();

ConstantClass cl = (ConstantClass)cp.getConstant(clazz.getClassNameIndex(),
Constants.CONSTANT_Class);
ConstantUtf8 name = (ConstantUtf8)cp.getConstant(cl.getNameIndex(),
Constants.CONSTANT_Utf8);
name.setBytes(class_name.replace('.', '/'));

return clazz;
}

createClass()中,通过subString()截取$$$BCEL$$$后的字符串,并调用Utility.decode进行相应的解码并最终返回改字节码的bytes数组(decode方法参数uncompress用来标识是否为zip流,当为true时走zip流解码)。之后生成Parser解析器并调用parse()方法进行解析,并生成JavaClass对象
createClass源码如下:

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
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);

JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

clazz = parser.parse();
} catch(Throwable e) {
e.printStackTrace();
return null;
}

// Adapt the class name to the passed value
ConstantPool cp = clazz.getConstantPool();

ConstantClass cl = (ConstantClass)cp.getConstant(clazz.getClassNameIndex(),
Constants.CONSTANT_Class);
ConstantUtf8 name = (ConstantUtf8)cp.getConstant(cl.getNameIndex(),
Constants.CONSTANT_Utf8);
name.setBytes(class_name.replace('.', '/'));

return clazz;
}

image-20230425212337403

这里就是前面为什么要进行字节码编码的原因,因为这里会进行解码

Utility.decode()源码

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
30
31
32
33
34
35
36
37
38
39
40
41
/** Decode a string back to a byte array.
*
* @param bytes the byte array to convert
* @param uncompress use gzip to uncompress the stream of bytes
*/
public static byte[] decode(String s, boolean uncompress) throws IOException {
char[] chars = s.toCharArray();

CharArrayReader car = new CharArrayReader(chars);
JavaReader jr = new JavaReader(car);

ByteArrayOutputStream bos = new ByteArrayOutputStream();

int ch;

while((ch = jr.read()) >= 0) {
bos.write(ch);
}

bos.close();
car.close();
jr.close();

byte[] bytes = bos.toByteArray();

if(uncompress) {
GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(bytes));

byte[] tmp = new byte[bytes.length * 3]; // Rough estimate
int count = 0;
int b;

while((b = gis.read()) >= 0)
tmp[count++] = (byte)b;

bytes = new byte[count];
System.arraycopy(tmp, 0, bytes, 0, count);
}

return bytes;
}

之后获取到了该JavaClass对象的bytes数组并调用java原生的defineClass()加载

image-20230425213242406

之后就是在newInstance()时初始化触发静态代码块执行

之后调用createClass()方法拿到一个JavaClass对象最终通过defineClass()加载字节码还原类。

这里就是为什么要用javaclass和utility编码的原因

上面的参考文章里还有一些题目是利用BCEl的,可以去参考参考一下