声明
以下内容,来自先知社区的LeeH作者原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。
前言
Apache Commons BCEL旨在为用户提供一种方便的方法来分析、创建和操作(二进制)Java类文件(以 .class 结尾的文件)。类由包含给定类的所有符号信息的对象表示:特别是方法、字段和字节码指令。
这些对象可以从现有文件中读取,由程序(例如运行时的类加载器)转换并再次写入文件,一个更有趣的应用是在运行时从头开始创建类
BCEL包含一个名为JustIce的字节码验证器,它通常会为您提供比标准JVM消息更好的关于代码错误的信息。
漏洞概述
首先看看apache list中的解释

Apache Commons BCEL有许多通常只允许更改特定类特征的API。但是,由于越界写入问题,这些API可用于生成任意字节码。这可能会在将攻击者可控制的数据传递给这些API的应用程序中被滥用,从而使攻击者能够比预期更多地控制生成的字节码
影响版本
< 6.6.0
漏洞分析
在这里我们可以看到漏洞的细节
https://github.com/apache/commons-bcel/pull/147
我们首先来了解一下bcel的用法
查看官方文档
Apache Commons BCEL™ – Home
我们从上面的修复位置可以知道主要是在常量池的位置造成的越界写入漏洞
我们直接关注到常量池的解释中去
在ClassGen中

该类是用来构建Java类的模板类。可以使用现有的java类(文件)进行初始化
而这个类是在org.apache.bcel.generic包下的一个类,这个包提供了一个用于动态创建或转换类文件的抽象级别。它使Java类文件的静态约束(如硬编码的字节码地址)变得“通用”。例如,通用常量池由类ConstantPoolGen实现,该类提供添加不同类型常量的方法。因此,ClassGen提供了一个接口来添加方法、字段和属性

对于ConstantPoolGen这个类用于构建常量池。用户通过“addXXX”方法、“addString”、“addClass”等方法添加常量。这些方法返回常量池的索引。最后,`getFinalConstantPool()' 返回建立起来的常量池。

该类中设计了多个addxxx方法可以添加不同类型的常量,最后通过调用getFinalConstantPool方法获取创建的常量池

之后通过调用ClassGen的相关API进行接下来的操作

看一下官方给出的关系图

对于漏洞的触发点,我们可以通过查看commit

在修复前,在对给定的常量数组进行初始化的时候,并没有限制传入的常量数组cs的大小
只是在默认的BUFFER大小为256和在传入的常量数组长度+64之后去了个最大的值作为了size
如果这时候传入的cs数组+64是大于65535这个临界值的时候将会导致越界漏洞的产生
所以对于漏洞的利用只需要在常量数组中前面写入足够长的垃圾数据,后面写入恶意常量数据,将会在通过getFinalConstantPool方法返回对应的ConstantPool对象之后调用其dump方法从二进制流到文件流的转换

就能够成功写入文件了
漏洞利用
我们可以简单写一个demo来通过Apache Com-mons BCEL API动态生成一个HelloWorld.class文件
package pers.bcel;import org.apache.bcel.generic.*;import org.aspectj.apache.bcel.Constants;import static org.apache.bcel.Const.ACC_PUBLIC;import static org.apache.bcel.Const.ACC_SUPER;import static org.apache.bcel.Constants.ACC_STATIC;public class TestBcel {public static void main(String[] args) {try {// BCEL Appendix A example// ClassGen(String class_name, String super_class_name, String// file_name, int access_flags, String[] interfaces)ClassGen cg = new ClassGen("HelloWorld", "java.lang.Object","<generated>", ACC_PUBLIC | ACC_SUPER, null);ConstantPoolGen cp = cg.getConstantPool();InstructionList il = new InstructionList();// create main method// MethodGen(int access_flags, Type return_type, Type[] arg_types,// String[] arg_names, String method_name, String class_name,// InstructionList il, ConstantPoolGen cp)MethodGen mg = new MethodGen(ACC_STATIC | ACC_PUBLIC, Type.VOID,new Type[] { new ArrayType(Type.STRING, 1) },new String[] { "argv" }, "main", "HelloWorld", il, cp);LocalVariableGen lg = null;InstructionFactory factory = new InstructionFactory(cg);// define some often used typesObjectType i_stream = new ObjectType("java.io.InputStream");ObjectType p_stream = new ObjectType("java.io.PrintStream");//%%//BufferedReader in = new BufferedReader(new InputStreamReader(System.in));//%%// create variables in and nameil.append(factory.createNew("java.io.BufferedReader"));il.append(InstructionConstants.DUP);il.append(factory.createNew("java.io.InputStreamReader"));il.append(InstructionConstants.DUP);//init input streamil.append(factory.createFieldAccess("java.lang.System", "in",i_stream, Constants.GETSTATIC));// for (int j = 0; j < 21845; j++) {// il.append(factory.createFieldAccess("java.lang.System", "in", i_stream, Constants.GETSTATIC));// }il.append(factory.createInvoke("java.io.InputStreamReader","<init>", Type.VOID, new Type[] { i_stream },Constants.INVOKESPECIAL));il.append(factory.createInvoke("java.io.BufferedReader", "<init>",Type.VOID, new Type[] { new ObjectType("java.io.Reader") },Constants.INVOKESPECIAL));//add in into the local variable pool and get the index automaticallylg = mg.addLocalVariable("in", new ObjectType("java.io.BufferedReader"), null, null);int in = lg.getIndex();//index of "in" varlg.setStart(il.append(new ASTORE(in)));//store the reference into local variable//首先创建对象,并初始化,操作结果在JVM的“堆”里,还需要在本地变量表中创建引用,因此在本地变量表中添加一个“in”比那辆,//然后根据索引值调用“astore”指令,即可将对象引用赋值给本地变量/*0: new #8; //class java/io/BufferedReader3: dup4: new #10; //class java/io/InputStreamReader7: dup8: getstatic #16; //Field java/lang/System.in:Ljava/io/InputStream;11: invokespecial #20; //Method java/io/InputStreamReader."<init>":(Ljava/io/InputStream;)V14: invokespecial #23; //Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V17: astore_1* *///%%//String name = null;//%%// create local variable name and init it to nulllg = mg.addLocalVariable("name", Type.STRING, null, null);int name = lg.getIndex();il.append(InstructionConstants.ACONST_NULL);//add "null" to the stack toplg.setStart(il.append(new ASTORE(name)));//"store" the value of "null" into "name" var//%%//System.out.print("Please enter your name> ")//%%// create try_catch blockInstructionHandle try_start = il.append(factory.createFieldAccess("java.lang.System", "out", p_stream, Constants.GETSTATIC));//从常量池中取出“please .....”,压入栈顶:这里感觉有问题,这个字符串常量应该先压入常量池才可以(最好是在这之前加一句,//加一句添加常量池操作其实并不影响实际运行的效率)il.append(new PUSH(cp, "Please enter your name> "));il.append(factory.createInvoke("java.io.PrintStream", "print",Type.VOID, new Type[] { Type.STRING },Constants.INVOKEVIRTUAL));//%%//name = in.readLine();//%%//将本地变量“in”推送至栈顶il.append(new ALOAD(in));il.append(factory.createInvoke("java.io.BufferedReader","readLine", Type.STRING, Type.NO_ARGS,Constants.INVOKEVIRTUAL));//调用readLine()方法il.append(new ASTORE(name));//接收的结果在栈顶,需要保存,因此加上保存到“name”slot的指令//%%// } catch(IOException e) { return; }//%%GOTO g = new GOTO(null);InstructionHandle try_end = il.append(g);//add return:如果出异常,才会走到这条“return”指令,并返回到caller中InstructionHandle handler = il.append(InstructionConstants.RETURN);// add exception handler which returns from the methodmg.addExceptionHandler(try_start, try_end, handler, null);//%%//没有异常,继续执行:System.out.println("Hello, " + name);//%%// "normal" code continues, set the branch target of the GOTOInstructionHandle ih = il.append(factory.createFieldAccess("java.lang.System", "out", p_stream, Constants.GETSTATIC));g.setTarget(ih);// print "Hello":创建一个StringBuffer对象,通过调用StringBuffer的append操作,实现//string1 + string2的操作,并且操作结果调用toString方法il.append(factory.createNew(Type.STRINGBUFFER));il.append(InstructionConstants.DUP);il.append(new PUSH(cp, "Hello, "));il.append(factory.createInvoke("java.lang.StringBuffer", "<init>",Type.VOID, new Type[] { Type.STRING },Constants.INVOKESPECIAL));il.append(new ALOAD(name));il.append(factory.createInvoke("java.lang.StringBuffer", "append",Type.STRINGBUFFER, new Type[] { Type.STRING },Constants.INVOKEVIRTUAL));//il.append(factory.createInvoke("java.lang.StringBuffer","toString", Type.STRING, Type.NO_ARGS,Constants.INVOKEVIRTUAL));il.append(factory.createInvoke("java.io.PrintStream", "println",Type.VOID, new Type[] { Type.STRING },Constants.INVOKEVIRTUAL));il.append(InstructionConstants.RETURN);// finalizationmg.setMaxStack();cg.addMethod(mg.getMethod());il.dispose();cg.addEmptyConstructor(ACC_PUBLIC);// dump the classcg.getJavaClass().dump("HelloWorld.class");System.out.println("dump successly");} catch (java.io.IOException e) {System.err.println(e);} catch (Exception e1) {e1.printStackTrace();}}}
运行上面的代码,可以得到一个类文件

主要是在后面通过cg.getJavaClass.dump中进而调用ConstantPool#dump方法写入文件

根据上面的描述,如果我们在前面添加一些垃圾数据,将会导致越界写入我这里通过添加65500个变量来作为垃圾数据进行填充
for (int j = 0; j < 65500; j++) {lg = mg.addLocalVariable("name" + j, Type.STRING, null, null);int name = lg.getIndex();il.append(InstructionConstants.ACONST_NULL);//add "null" to the stack toplg.setStart(il.append(new ASTORE(name)));//"store" the value of "null" into "name" var}
通过调试,我们可以知道这时候的constant-Pool.length=65570

将会导致多余的常量越界写入,这里我就是任意的二进制数据,可以精心构造一个完整的class文件
漏洞修复
官方通过增加上限的方式在关键方法增加了判断
比如在ConstantPoolGen的构造方法中初始化常量数组的过程中
又或者是在adjustSize方法中添加判断

最后在调用dump方法进行转换的时候也进行了限制

尝试进行写入:

将会抛出异常
欢迎关注长白山攻防实验室
定期更新优质文章分享
本文分析了Apache Commons BCEL中存在的越界写入漏洞,详细介绍了如何利用该漏洞生成恶意字节码,以及官方的修复措施。
1012

被折叠的 条评论
为什么被折叠?



