DataOutputStream

DataOutputStream使用的是直接写入的方式,也就是说DataOutputStream.write(byte[])是直接将数据追加到数据的末位,所以如果使用这种方式,在DataInputStream转换为ObjectInputStream时,此时产生四字节的Magic Header读取。

这也就是为什么在南北ERP#反序列化漏洞的反序列化构造时,直接在前面的固定头写入数据即可,因为DataInputStream是在最终的反序列化对象时才转换的ObjectInputStream,这也就使得我们无需修改地直接拼接即可。

ObjectOutputStream

Magic Header

ObjectOutputStream在开始写入之时,即在流首会添加4直接的Magic Header,一般为:

AC ED 00 05

其中AC ED为Magic number,表明这是Java的序列化流,00 05则表示这是一个Java 1.5格式的序列化流。

不同数据的Output方式

ObjectOutputStream在写入数据时,区分Object和BlockData,这是一个非常重要的性质

ObjectOutputStream在序列化数据时,注意,将连续的常规的数据类型都写入BlockData,若干个常规类型将组成一个BlockData。

譬如:

os.writeUTF("123");
os.writeUTF("456");

这实际上会写入到一个BlockData中;BlockData的组织形式和DataOutputStream一致。

为了区分BlockData和Object,需要写入Block Data Header以及长度,TC_BLOCKDATA=0x77,所以会先写入一个0x77后再写入一字节的长度,然后就是DataOutputStream写入具体的Block Data数据。

而写入对象时,则会先写入一个Object Header,然后按照对象的序列化规则写入:

  1. Object Header TC_OBJECT 0x73
  2. Class Descriptor TC_CLASSDESC 0x72
  3. Class name length 2 bytes (calc as n)
  4. Class name n bytes (full class name)
  5. … (过程过多,了解到此处即可)

所以可以得到一个结论,在ObjectOutputStream中,无整体结构组织(因为各部分的数据都依赖于Header Byte区分),仅有部分结构的组织,即各部分不影响;这就使得我们拼接序列化数据成为可能。

从这里我们就能知道了,如果说目标的反序列化漏洞前面加入了一些readXXX,那么我们只需要按需构造writeXXX,然后拼接yso的payload即可。

不过从上面的Magic Header也能知道,yso生成的其实是有4字节的Magic Header的,因此再拼接时需要去掉前4字节即可。

ObjectOutputStream.write追加问题

在实际拼接中,由于Java代码通常建议仅对一个包装的上层封装流进行操作,因此可能会出现这样的情况:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(baos);
os.writeUTF("123");
os.writeByte(1);
byte[] actData = new byte[ysoBytes.length - 4];  
System.arraycopy(ysoBytes, 4, actData, 0, ysoBytes.length - 4);
os.write(actData);

而这样就出现了一个非常严重的问题,在上面说过,ObjectOutputStream会将连续的、常规的数据,写入到一个BlockData,因此这里的拼接会导致,写入的是在BlockData中的一个byte数组而不是对象序列化数据。

解决办法也很简单:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(baos);
os.writeUTF("123");
os.writeByte(1);
byte[] actData = new byte[ysoBytes.length - 4];  
System.arraycopy(ysoBytes, 4, actData, 0, ysoBytes.length - 4);
os.close()
baos.write(actData);

先关闭ObjectOutputStream然后再用baos写入即可。

写入一个BlockData,注意,如果不关闭流,ObjectOutputStream会将数据写在缓冲区而非写入流,因为下一个数据仍然可能是常规数据,也可能需要写入BlockData。