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,然后按照对象的序列化规则写入:
- Object Header ⇒ TC_OBJECT 0x73
- Class Descriptor ⇒ TC_CLASSDESC 0x72
- Class name length ⇒ 2 bytes (calc as n)
- Class name ⇒ n bytes (full class name)
- … (过程过多,了解到此处即可)
所以可以得到一个结论,在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。