参考
https://xz.aliyun.com/t/2744#reply-16082
首先要让服务端有动态地将字节流解析成Class的能力,这是基础。
正常情况下,Java并没有提供直接解析class字节数组的接口。不过classloader内部实现了一个protected的defineClass方法,可以将byte[]直接转换为Class,方法原型如下
这个类是classloader里的,classloader是将class字节类载入虚拟机的一种形式,为了给外界提供一种加载class的途径。比如可以从网络上加载。需要同时输入类名字是java的一种规范,即字节流是一个什么类,因为如果是jar,根据包名即可知道类名,但是字节流在没有载入jvm的时候,若能告诉类名,则可以更快的返回类型。当然,如果你实在不知道类名,那也没有关系,可以为null,通常这样是可以执行的,只是这样jvm需要把所有的字节码载入完毕后再设置类名,这样会多些步骤,也会多些消耗。而且还有一种情况就是,考虑到以后也可能会在字节码中并不携带类名的可能,所以就提供了这种实现方式,但是如果是这种情况就必须指定类名了。这个方法是通过jni实现的,属于jvm的代码,其功能就是通过字节码定义类,所以是通过调用C/C++实现的,可以下载jvm源码了解详情,这里我就不多介绍了。另:同样一个类名,不同的字节码可以多次定义,我就通过这个功能实现了类似反射的功能,调用的效率比java本身的反射功能高效20倍。当然,我的这种代码只是为了高效实现一个动态语言功能,或许反射的重复调用效率或者其他场景下效率会比这种情况高,我没有办法知道这种高效场景,毕竟我不喜欢使用反射。
因为该方法是protected的,我们没办法在外部直接调用,当然我们可以通过反射来修改保护属性,不过我们选择一个更方便的方法,直接自定义一个类继承classloader,然后在子类中调用父类的defineClass方法。
下面我们写个demo来测试一下:
Payload.java
package define.classs; import java.io.IOException; public class Payload { public String toString() { try { Runtime.getRuntime().exec("notepad.exe"); }catch(IOException e) { e.printStackTrace(); } return "seven god"; } }
Go.java
package define.classs; import sun.misc.BASE64Decoder; public class Go { public static class Myloader extends ClassLoader //继承ClassLoader { public Class get(byte[] b) { return super.defineClass(b, 0, b.length); } } public static void main(String[] args) throws Exception { // TODO Auto-generated method stub String classStr="yv66vgAAADQAKAEAFWRlZmluZS9jbGFzc3MvUGF5bG9hZAcAAQEAEGphdmEvbGFuZy9PYmplY3QHAAMBAAxQYXlsb2FkLmphdmEBAAY8aW5pdD4BAAMoKVYMAAYABwoABAAIAQAEdGhpcwEAF0xkZWZpbmUvY2xhc3NzL1BheWxvYWQ7AQAIdG9TdHJpbmcBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEAE2phdmEvaW8vSU9FeGNlcHRpb24HAA4BABFqYXZhL2xhbmcvUnVudGltZQcAEAEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMABIAEwoAEQAUAQALbm90ZXBhZC5leGUIABYBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAYABkKABEAGgEAD3ByaW50U3RhY2tUcmFjZQwAHAAHCgAPAB0BAAlzZXZlbiBnb2QIAB8BAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQAEQ29kZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAD0xpbmVOdW1iZXJUYWJsZQEADVN0YWNrTWFwVGFibGUBAApTb3VyY2VGaWxlACEAAgAEAAAAAAACAAEABgAHAAEAIwAAAC8AAQABAAAABSq3AAmxAAAAAgAkAAAADAABAAAABQAKAAsAAAAlAAAABgABAAAABAABAAwADQABACMAAABpAAIAAgAAABS4ABUSF7YAG1enAAhMK7YAHhIgsAABAAAACQAMAA8AAwAkAAAAFgACAAAAFAAKAAsAAAANAAQAIQAiAAEAJQAAABIABAAAAAcACQAIAA0ACQARAAsAJgAAAAcAAkwHAA8EAAEAJwAAAAIABQ=="; BASE64Decoder code=new sun.misc.BASE64Decoder(); Class result=new Myloader().get(code.decodeBuffer(classStr));//将base64解码成byte数组,并传入t类的get函数 System.out.println(result.newInstance().toString()); } }
简单解释一下上述代码: 首先自定义一个类Myloader,并继承classloader父类,然后自定义一个名为get的方法,该方法接收byte数组类型的参数,然后调用父类的defineClass方法去解析byte数据,并返回解析后的Class。 单独编写一个Payload类,并实现toString方法。因为我们想要我们的服务端尽可能的短小精悍,所以我们定义的Payload类即为默认的Object的子类,没有额外定义其他方法,因此只能借用Object类的几个默认方法,由于我们执行payload之后还要拿到执行结果,所以我们选择可以返回String类型的toString方法。把这个类编译成Payload.class文件。 main函数中classStr变量为上述Payload.class文件二进制流的base64编码。 新建一个Myloader的实例,将classStr解码为二进制字节流,并传入Myloader实例的get方法,得到一个Class类型的实例result,此时result即为Payload.class(注意此处的Payload.class不是上文的那个二进制文件,而是Payload这个类的class属性)。 调用result类的默认无参构造器newInstance()生成一个Payload类的实例,然后调用该实例的toString方法,继而执行toString方法中的代码:Runtime.getRuntime().exec("calc.exe");return “OK” 在控制台打印出toString方法的返回值。 OK,代码解释完了,下面尝试执行Demo类,成功弹出计算器,并打印出“OK”字符串,如下图:
到这里就实现了我们动态解析执行class字节流了。