java web安全(自定义Classloader)--服务器端动态解析二进制class文件

参考

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字节流了。

posted @ 2021-02-04 13:27  yourse1f  阅读(279)  评论(0编辑  收藏  举报