[备份]终极Java反序列化Payload缩小技术

介绍

实战中由于各种情况,可能会对反序列化Payload的长度有所限制,因此研究反序列化Payload缩小技术是有意义且必要的

本文以CommonsBeanutils1链为示例,重点在于三部分:

接下来我将展示如何一步一步地缩小

最终效果能够将YSOSERIAL生成的Payload缩小接近三分之二(从3692长度缩小到1296

 

YSOSERIAL

首先用YSOSERIAL工具直接生成CB1的链,看看Base64处理后的长度

生成后统计长度为:3692

 

构造Gadget

尝试不借助YSOSERIAL直接构造CB1的链

构造代码

恶意类

读取字节码并设置到Gadget中,序列化后统计长度:2728

相比YSOSERIAL直接生成的,缩小了26.1%

其实上文中还有三处可以优化:

经过这三处优化后得到长度:2608

相比YSOSERIAL直接生成的,缩小了29.3%

 

从字节码层面优化

上文中的EvilBytesCode恶意类的字节码是可以缩减的

对字节码进行分析:javap -c -l EvilByteCodes.class

可以看出,该类每个方法包含了三部分:

有JVM相关的知识可以得知,局部变量表和异常表是不能删除的,否则无法执行

LineNumberTable是可以删除的

换句话来说:LINENUMBER指令可以全部删了

于是我基于ASM实现删除LINENUMBER

ShortClassVisitor

重点在于ShortMethodAdapter:如果遇到LINENUMBER指令则阻止传递,可以理解为返回空

读取编译的字节码并处理后替换

经过优化后得到长度:1832

相比YSOSERIAL直接生成的,缩小了50.3%

 

使用Javassist构造

以上代码虽然做到了超过百分之五十的缩小,但存在一个问题:目前的恶意类是写死的,无法动态构造

想要动态构造字节码一种手段是选择ASM做,但有更好的选择:Javassist

通过这样的一个方法,就可以根据输入命令动态构造出Evil

将动态生成的字节码保存至当前目录,再读取加载

经过优化后得到长度:1848

相比YSOSERIAL直接生成的,缩小了49.9%

不难发现使用Javassist生成的字节码似乎本身就不包含LINENUMBER指令

不过这只是猜测,当我使用上文的删除指令代码优化后,发现进一步缩小了

经过优化后得到长度:1804

相比YSOSERIAL直接生成的,缩小了51.1%

验证Payload有效可以弹出计算器

 

删除重写方法

可以发现Evil类继承自AbstractTranslet抽象类,所以必须重写两个transform方法

这样写代码会导致编译不通过,无法执行

编译不通过不代表非法,通过手段直接构造对应的字节码

(1)通过ASM删除方法

(2)通过Javassist直接构造

通过以上手段处理后进行反序列化验证:成功弹出计算器

最终优化后得到长度:1332

相比YSOSERIAL直接生成的,缩小了63.9%

并不是所有方法都能删除,比如不存在构造方法的情况下无法删除空参构造

于是有了一个新思路:删除静态代码块,将代码写入空参构造

最终优化后得到长度:1296

相比YSOSERIAL直接生成的,缩小了64.8%

 

终极技术:分块传输

以上的内容都在围绕字节码和序列化数据的缩小,我认为已经做到的接近极致,很难做到更小的

对于STATIC代码块中需要执行的代码也有缩小手段,这也是更有实战意义是思考,因为实战中不是弹个计算器这么简单

因此可以用追加的方式发送多个请求往指定文件中写入字节码,将真正需要执行的字节码分块

使用Javassist动态生成写入每一分块的Payload,以追加的方式将所有字节码的Base64写入某文件

在最后一个包中将字节码进行Base64Decode并写入class文件

(也可以直接写字节码二进制数据,不过个人认为Base64好分割处理一些)

会有师傅产生疑问:为什么要写这么多的代码而不用java.nio.file.Files工具类一行实现读写

其实我一开始就是使用该工具类在做,后来测试发现使用Stream读写产生的Payload会更小

最后一个包使用URLClassLoader进行加载

注意一个小坑,传入URLClassLoader的路径要以file://开头且以/结尾否则会找不到对应的类

测试

我对常见的反序列化链做了总结和测试,效果如下(出了个叛徒)