0x01 起因
关于Java反序列化的文章已经相当的多了,而且大家也对于这个东西说的很清楚了,所以今年我想换个角度来看看这个东西。我们都知道Java反序列化漏洞的产生原因在于开发者在重写 readObject 方法的时候,写入了漏洞代码,这个和PHP的反序列化漏洞很像,在反序列化的时候出发了在 __destruct 等魔术函数中的漏洞代码。这里就有一个问题了,先看一下我的demo吧, ObjectCalc.java 为重写 readobject 方法文件。
那么我通过下面的代码可以触发反序列化漏洞,弹出计算器。
那么问题来了我们通过 第8行 ois.readObject 获取到的输入流过程中调用了 readObject 方法,为什么最后会调用到被反序列化类( ObjectCalc )中的 readObject 方法,这个 readObject 调用过程到底是怎么样的。
0x02 深入分析
为了弄清楚这个问题,我决定在ObjectCalc.java文件中的命令执行位置下一个断点,好的相关调用栈已经出来了,这时候我们跟进一下。
先跟进一下 ObjectInputStream.readObject ,这里我简化了一下代码,关键位置在 第431行 调用了 readObject0 方法,并且传入false。
继续跟进一下 readObject0 方法,关键在下面这两行,此时的 TC_OBJECT 的值为115,且调用了 readOrdinaryObject 方法。
case TC_OBJECT:return checkResolve(readOrdinaryObject(unshared));
跟进 readOrdinaryObject 方法,调用了 readSerialData 方法。
private Object readOrdinaryObject(boolean unshared)throws IOException{...if (desc.isExternalizable()) {readExternalData((Externalizable) obj, desc);} else {readSerialData(obj, desc);}
继续跟进一下readSerialData方法,该方法的实现如下所示。
动态调试结果来看,重写 readObject 会进入第14行的 slotDesc.invokeReadObject 方法中,再跟进一下 slotDesc.invokeReadObject 方法,该方法主要代码如下:
void invokeReadObject(Object obj, ObjectInputStream in)throws ClassNotFoundException, IOException,UnsupportedOperationException{requireInitialized();if (readObjectMethod != null) {try {readObjectMethod.invoke(obj, new Object[]{ in });
其中readObjectMethod.invoke这个方法很熟悉了,java的反射机制,也就说通过重写readObject的整个调用流程会进过java的反射机制。
这里再看一个不通过重写 readObject 反序列化的调用过程,我省略了前面的跟踪调试过程,大家看下图。
不通过重写 readObject 的反序列化过程一样是进入 readSerialData 中,但是是通过 defaultReadFields 进行处理,这里有个关注点是 slotDesc.hasReadObjectMethod() 返回的结果是false,也就是下面这个if判断的结果,我简化了一下流程。
else if (slotDesc.hasReadObjectMethod()) {slotDesc.invokeReadObject(obj, this);...} else {defaultReadFields(obj, slotDesc);}
也就是说实际上是否重写了 readObject 影响的是slotDesc.hasReadObjectMethod()的结果,那么跟进一下 hasReadObjectMethod 方法,这里我在return (readObjectMethod != null);下了一个断点,对比一下重写 readObject 结果和不重写 readObject 结果的差别,第一张图是不重写 readObject ,第二张图是重写 readObject 。
很明显我们发现了返回结果不一样,第一张图的结果自然return为false,第二张图return结果自然为true,也就是说重写 readObject 结果和不重写 readObject 结果的差别本质上在于进入的循环不一样。
0x03 小结
根据上面的动态调试结果,简单做个小结,也就是说如果反序列化的过程中被反序列化类重写了 readObject ,该数据在反序列化的过程中核心流程走到 readSerialData 方法中的 slotDesc.invokeReadObject 方法,通过反射机制触发相关流程,并且调用重写的 readObject 。如果没有重写 readObject ,则调用 ObjectInputStream 类中的 readObject 方法,并且执行反序列化。
往期精彩
感兴趣的可以点个关注!!!
关注「安全先师」
把握前沿安全脉搏