参考文章
前言 BCEL平常在测试反序列化的时候也经常会用到,比如延时测Gadget以及在某些场景下执行命令不是那么顺手的情况下选择BCEL去打内存马,就像Fastjson和Thymeleaf SSTI这种。以前也只是用到这个BCEL但是没有仔细学习过,下面简单学习记录下BCEL。
Java安全之BCEL ClassLoader 目录
写在前面# BCEL平常在测试反序列化的时候也经常会用到,比如延时测Gadget以及在某些场景下执行命令不是那么顺手的情况下选择BCEL去打内存马,就像Fastjson和Thymeleaf SSTI这种。以前也只是用到这个BCEL但是没有仔细学习过,下面简单学习记录下BCEL。
About BCEL BCEL Classloader在 JDK < 8u251之前是在rt.jar里面。 同时在Tomcat中也会存在相关的依赖 tomcat7
org.apache.tomcat.dbcp.dbcp.BasicDataSource
tomcat8及其以后
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
而在rt.jar!/com/sun/org/apache/bcel/internal/util/
包下,有Classloader
这么一个类,可以实现加载字节码并初始化一个类的功能,该类也是个Classloader(继承了原生的Classloader类)重写了loadClass()
方法,源码如下:
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 protected Class loadClass (String class_name, boolean resolve) throws ClassNotFoundException { Class cl = null ; if ((cl=(Class)classes.get(class_name)) == null ) { for (int i=0 ; i < ignored_packages.length; i++) { if (class_name.startsWith(ignored_packages[i])) { cl = deferTo.loadClass(class_name); break ; } } if (cl == null ) { JavaClass clazz = null ; if (class_name.indexOf("$$BCEL$$" ) >= 0 ) clazz = createClass(class_name); else { if ((clazz = repository.loadClass(class_name)) != null ) { clazz = modifyClass(clazz); } else throw new ClassNotFoundException (class_name); } if (clazz != null ) { byte [] bytes = clazz.getBytes(); cl = defineClass(class_name, bytes, 0 , bytes.length); } else cl = Class.forName(class_name); } if (resolve) resolveClass(cl); } classes.put(class_name, cl); return cl; }
首先会判断类名是否以$$$BCEL$$$开头,之后调用createClass()
方法拿到一个JavaClass
对象最终通过defineClass()
加载字节码还原类。
调试分析 先来看下简单的使用,在同一包下,准备一个恶意类
1 2 3 4 5 6 7 8 9 10 11 12 13 package MemoryShell.BCEL;import java.io.IOException;public class calc { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } }
准备一个BCEL的demo,运行即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package MemoryShell.BCEL;import com.sun.org.apache.bcel.internal.Repository;import com.sun.org.apache.bcel.internal.classfile.JavaClass;import com.sun.org.apache.bcel.internal.classfile.Utility;import com.sun.org.apache.bcel.internal.util.ClassLoader;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(),首先会判断类名是否以$$$BCEL$$$开头,之后调用createClass()
方法拿到一个JavaClass
对象最终通过defineClass()
加载字节码还原类。
这里原因就是刚开始对loadClass的分析
那么我们就进行断点调试跟一下
这里由于我们前面给文件头加了个BCEl字节码,于是这里就可以进入createClass()
createClass源码
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 protected JavaClass createClass (String class_name) { int index = class_name.indexOf("$$BCEL$$" ); String real_name = class_name.substring(index + 8 ); JavaClass clazz = null ; try { byte [] bytes = Utility.decode(real_name, true ); ClassParser parser = new ClassParser (new ByteArrayInputStream (bytes), "foo" ); clazz = parser.parse(); } catch (Throwable e) { e.printStackTrace(); return null ; } ConstantPool cp = clazz.getConstantPool(); ConstantClass cl = (ConstantClass)cp.getConstant(clazz.getClassNameIndex(), Constants.CONSTANT_Class); ConstantUtf8 name = (ConstantUtf8)cp.getConstant(cl.getNameIndex(), Constants.CONSTANT_Utf8); name.setBytes(class_name.replace('.' , '/' )); return clazz; }
createClass()
中,通过subString()
截取$$$BCEL$$$后的字符串,并调用Utility.decode
进行相应的解码并最终返回改字节码的bytes数组(decode方法参数uncompress用来标识是否为zip流,当为true时走zip流解码)。之后生成Parser
解析器并调用parse()
方法进行解析,并生成JavaClass
对象 createClass源码如下:
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 protected JavaClass createClass (String class_name) { int index = class_name.indexOf("$$BCEL$$" ); String real_name = class_name.substring(index + 8 ); JavaClass clazz = null ; try { byte [] bytes = Utility.decode(real_name, true ); ClassParser parser = new ClassParser (new ByteArrayInputStream (bytes), "foo" ); clazz = parser.parse(); } catch (Throwable e) { e.printStackTrace(); return null ; } ConstantPool cp = clazz.getConstantPool(); ConstantClass cl = (ConstantClass)cp.getConstant(clazz.getClassNameIndex(), Constants.CONSTANT_Class); ConstantUtf8 name = (ConstantUtf8)cp.getConstant(cl.getNameIndex(), Constants.CONSTANT_Utf8); name.setBytes(class_name.replace('.' , '/' )); return clazz; }
这里就是前面为什么要进行字节码编码的原因,因为这里会进行解码
Utility.decode()源码
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 public static byte [] decode(String s, boolean uncompress) throws IOException { char [] chars = s.toCharArray(); CharArrayReader car = new CharArrayReader (chars); JavaReader jr = new JavaReader (car); ByteArrayOutputStream bos = new ByteArrayOutputStream (); int ch; while ((ch = jr.read()) >= 0 ) { bos.write(ch); } bos.close(); car.close(); jr.close(); byte [] bytes = bos.toByteArray(); if (uncompress) { GZIPInputStream gis = new GZIPInputStream (new ByteArrayInputStream (bytes)); byte [] tmp = new byte [bytes.length * 3 ]; int count = 0 ; int b; while ((b = gis.read()) >= 0 ) tmp[count++] = (byte )b; bytes = new byte [count]; System.arraycopy(tmp, 0 , bytes, 0 , count); } return bytes; }
之后获取到了该JavaClass
对象的bytes数组并调用java原生的defineClass()
加载
之后就是在newInstance()
时初始化触发静态代码块执行
之后调用createClass()
方法拿到一个JavaClass
对象最终通过defineClass()
加载字节码还原类。
这里就是为什么要用javaclass和utility编码的原因
上面的参考文章里还有一些题目是利用BCEl的,可以去参考参考一下