Java反序列化过程深究 0x01 概述 互联网上大家针对Java反序列化的讨论有很多了,但是这里我还是想聊聊,这里仅仅记录一下自己的学习笔记,之前我在Java 反序列化深究 中讨论了为什么,通过重写 readObject 方法会导致反序列化的问题,当然后续整个过程我们也没继续,当然现在继续回来讨论这个事情。
0x02 反序列化过程 这里需要配合我们的反序列化字节流里面的数据来看。
而下面是整个过程的调用栈,我们一点点来看。
1 2 3 4 5 6 7 8 9 10 11 12 exec:347, Runtime (java.lang) readObject:11, ObjectCalc invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:497, Method (java.lang.reflect) invokeReadObject:1017, ObjectStreamClass (java.io) readSerialData:1896, ObjectInputStream (java.io) readOrdinaryObject:1801, ObjectInputStream (java.io) readObject0:1351, ObjectInputStream (java.io) readObject:371, ObjectInputStream (java.io) main:11, unSerializableCalc
在 ObjectInputStream#readObject0 ,会根据 tc 的 byte 值进入 switch 中,选择相关的 case 进行下一步操作。回到之前反序列化字节流中,我们可以看到这里的 TC_OBJECT 为 0x73 ,所以这里会进入 case 为 TC_OBJECT 中 readOrdinaryObject 的进行字节流的处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 10 - 0x00 0a Value - ObjectCalc - 0x4f626a65637443616c63 serialVersionUID - 0x33 73 1c 20 ac f0 18 3b newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 0 - 0x00 00 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 01 classdata ObjectCalc values
跟进 readOrdinaryObject ,在 readOrdinaryObject 中会根据字节流中的下一个关键字 TC_CLASSDESC 进行相关处理,这个 TC_CLASSDESC 是一个 类描述符标识 ,我们可以看到 TC_CLASSDESC 中的 classname 的value正是我们前面测试代码中的 ObjectCalc 这个类名字。
1 2 3 4 TC_CLASSDESC - 0x72 className Length - 10 - 0x00 0a Value - ObjectCalc - 0x4f626a65637443616c63
而处理上述这些东西的方法自然是 readClassDesc ,这里画个重点,下面反序列化的一些修复方法其实和这个里面的一些有关系,当然这里面还有个 TC_PROXYCLASSDESC 引起了我的注意,这个 TC_PROXYCLASSDESC 是 代理类描述符 ,看到代理类这三个字相信熟悉反序列化的朋友们可能不会陌生,当然不太熟悉的朋友可以了解一下 ysoserial 这个项目,这个项目中大量使用了动态代理方式,这种方式反序列化利用本次不会深入探讨,所以话说回来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private Object readOrdinaryObject (boolean unshared) throws IOException { if (bin.readByte() != TC_OBJECT) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false ); desc.checkDeserialize(); private ObjectStreamClass readClassDesc (boolean unshared) throws IOException { byte tc = bin.peekByte(); switch (tc) { case TC_NULL: return (ObjectStreamClass) readNull(); case TC_REFERENCE: return (ObjectStreamClass) readHandle(unshared); case TC_PROXYCLASSDESC: return readProxyDesc(unshared); case TC_CLASSDESC: return readNonProxyDesc(unshared); default : throw new StreamCorruptedException( String.format("invalid type code: %02X" , tc)); } }
这里我们分别跟进一下 readProxyDesc 和 readNonProxyDesc 方法,首先跟进 readNonProxyDesc 方法,我删除部分代码,可以看看下面代码中的 resolveClass , resolveClass 处理的 readDesc 正是我们要利用的一个类 OjectClac 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private ObjectStreamClass readNonProxyDesc (boolean unshared) throws IOException ... Class<?> cl = null ; ClassNotFoundException resolveEx = null ; bin.setBlockDataMode(true ); final boolean checksRequired = isCustomSubclass(); try { if ((cl = resolveClass(readDesc)) == null ) { resolveEx = new ClassNotFoundException("null class" ); } else if (checksRequired) { ReflectUtil.checkPackageAccess(cl); } }
而继续跟进 resolveClass 方法,可以看到通过 Class.forName 方法反射调用我们的利用类 ObjcetCalc 。
1 2 3 4 5 6 7 protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); try { return Class.forName(name, false , latestUserDefinedLoader()); }
再回头看看 readProxyDesc 这个类,其实 readProxyDesc 和 readNonProxyDesc 在写法上会发现很像,唯一的区别就是在 resolveProxyClass 这个类上。
1 2 3 4 5 6 7 8 9 private ObjectStreamClass readProxyDesc (boolean unshared) throws IOException ... ObjectStreamClass desc = new ObjectStreamClass();... try { if ((cl = resolveProxyClass(ifaces)) == null ) { resolveEx = new ClassNotFoundException("null class" ); }
跟进 resolveProxyClass 中,这个方法最后会调用 Proxy.getProxyClass 来处理,比如获取代理类这些操作。
1 2 3 4 5 6 7 8 protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { ... try { return Proxy.getProxyClass( hasNonPublicInterface ? nonPublicLoader : latestLoader, classObjs);
当然经过 readProxyDesc 或者 readNonProxyDesc 处理之后实际上完成了类的实例化,也就说经过 readClassDesc 处理之后,完成了类的实例化,代码继续向下处理,标记了一些注释,这里自然是进入到 readSerialData 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private Object readOrdinaryObject (boolean unshared) throws IOException ObjectStreamClass desc = readClassDesc(false ); desc.checkDeserialize(); Class<?> cl = desc.forClass(); Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null ; } ... if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); }
Externalizable类型的反序列化类型,可以通过writeExternal()和readExternal()方法指定一个类的部分数据进行序列化与反序列化。 Serializable接口也可以实现类似的机制:将不想要序列化的部分添加一个关键字:transient(临时的)。它声明的变量实行序列化操作的时候不会写入到序列化文件中去。
在 readSerialData 中有一个 slotDesc.hasReadObjectMethod 判断,而我们在《Java 反序列化深究》讨论过就是它判断是否反序列化,跟进 hasReadObjectMethod 实际上是判断 readObjectMethod 是否为null,并且结果是 boolean 型。
1 2 3 boolean hasReadObjectMethod () { return (readObjectMethod != null ); }
这个 readObjectMethod 如何来的,实际上需要追溯到前面 readClassDesc 和 readNonProxyDesc 中,实际上,这两个方法实例化类之后,都有一个 desc.initNonProxy 构造方法来处理结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 try { if ((cl = resolveClass(readDesc)) == null ) { resolveEx = new ClassNotFoundException("null class" ); } else if (checksRequired) { ReflectUtil.checkPackageAccess(cl); } } catch (ClassNotFoundException ex) { resolveEx = ex; } skipCustomData(); desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false )); try { if ((cl = resolveProxyClass(ifaces)) == null ) { resolveEx = new ClassNotFoundException("null class" ); } else if (!Proxy.isProxyClass(cl)) { throw new InvalidClassException("Not a proxy" ); } else { ReflectUtil.checkProxyPackageAccess( getClass().getClassLoader(), cl.getInterfaces()); } } catch (ClassNotFoundException ex) { resolveEx = ex; } skipCustomData(); desc.initProxy(cl, resolveEx, readClassDesc(false ));
跟进 initNonProxy 构造方法,实际上这两个方法都会调用 lookup 方法处理 cl ,而 cl 实际上就是我们实例化的那个类,在这个例子中是 ObjectCalc 类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void initNonProxy (ObjectStreamClass model, Class<?> cl, ClassNotFoundException resolveEx, ObjectStreamClass superDesc) throws InvalidClassException { this .cl = cl; if (cl != null ) { localDesc = lookup(cl, true ); void initProxy (Class<?> cl, ClassNotFoundException resolveEx, ObjectStreamClass superDesc) throws InvalidClassException { this .cl = cl; if (cl != null ) { localDesc = lookup(cl, true );
跟进 lookup 在实例化 ObjectStreamClass ,处理了 cl 对象,而这个 cl 在我们这里面就是 ObjectCalc 。
1 2 3 4 5 6 7 8 9 10 11 static ObjectStreamClass lookup (Class<?> cl, boolean all) { if (!(all || Serializable.class .isAssignableFrom (cl ))) { return null ; } ... if (entry == null ) { try { entry = new ObjectStreamClass(cl); } catch (Throwable th) { entry = th; }
跟进 ObjectStreamClass ,可以看到而这个 cl 在我们这里面就是 ObjectCalc ,而这里会有个判断是否 externalizable ,这个属性我们前面说过了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private ObjectStreamClass (final Class<?> cl) { this .cl = cl; name = cl.getName(); isProxy = Proxy.isProxyClass(cl); isEnum = Enum.class .isAssignableFrom (cl ) ; serializable = Serializable.class .isAssignableFrom (cl ) ; externalizable = Externalizable.class .isAssignableFrom (cl ) ; ... if (serializable) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run () { ... if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); writeObjectMethod = getPrivateMethod(cl, "writeObject" , new Class<?>[] { ObjectOutputStream.class }, Void .TYPE ) ; readObjectMethod = getPrivateMethod(cl, "readObject" , new Class<?>[] { ObjectInputStream.class }, Void .TYPE ) ;
跟进 getPrivateMethod 方法,可以看到他会对一些属性进行判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private static Method getPrivateMethod (Class<?> cl, String name, Class<?>[] argTypes, Class<?> returnType) { try { Method meth = cl.getDeclaredMethod(name, argTypes); meth.setAccessible(true ); int mods = meth.getModifiers(); return ((meth.getReturnType() == returnType) && ((mods & Modifier.STATIC) == 0 ) && ((mods & Modifier.PRIVATE) != 0 )) ? meth : null ; } catch (NoSuchMethodException ex) { return null ; } }
比如拿下面这个例子为例子:
1 readObjectMethod = getPrivateMethod(cl, "readObject" ,new Class<?>[] { ObjectInputStream.class },
方法名为readObject
返回类型为void
传入参数为一个ObjectInputStream.class类型参数
修饰符不能包含static
修饰符必须包含private
所以说满足这种情况下才会进入反序列化的下一步核心 slotDesc.invokeReadObject 中,否则会进入 defaultReadFields 进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private void readSerialData (Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0 ; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slots[i].hasData) { if (obj != null && slotDesc.hasReadObjectMethod() && handles.lookupException(passHandle) == null ) { SerialCallbackContext oldContext = curContext; try { curContext = new SerialCallbackContext(obj, slotDesc); bin.setBlockDataMode(true ); slotDesc.invokeReadObject(obj, this ); } catch (ClassNotFoundException ex) { ... } else { defaultReadFields(obj, slotDesc); }
跟进 invokeReadObject ,最后可以看到调用 readObjectMethod.invoke 实际上再往下走就是一些反射方法了。
1 2 3 4 5 6 7 8 9 void invokeReadObject (Object obj, ObjectInputStream in) throws ClassNotFoundException, IOException, UnsupportedOperationException { if (readObjectMethod != null ) { try { readObjectMethod.invoke(obj, new Object[]{ in }); } }
再回到 ObjectInputStream#readSerialData 中,我们讨论了 slots[i].hasData 为 true 的情况下,实际上当这个 if 为 false 的时候来到的是 slotDesc.invokeReadObjectNoData 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void readSerialData (Object obj, ObjectStreamClass desc) throws IOException { if (slots[i].hasData) { bin.setBlockDataMode(true ); slotDesc.invokeReadObject(obj, this ); } if (slotDesc.hasWriteObjectData()) { skipCustomData(); } else { bin.setBlockDataMode(false ); } } else { if (obj != null && slotDesc.hasReadObjectNoDataMethod() && handles.lookupException(passHandle) == null ) { slotDesc.invokeReadObjectNoData(obj); }
而这个判断是实际是根据序列化的时候是不是重写了 readObjectNoData 来进行反序列化。
1 2 readObjectNoDataMethod = getPrivateMethod(cl, "readObjectNoData" , null , Void.TYPE); hasWriteObjectData = (writeObjectMethod != null );
再回到我们前面讨论 readOrdinaryObject ,我们讨论了序列化接口不是 Externalizable 类型,如果是的话自然会进入 readExternalData 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private Object readOrdinaryObject (boolean unshared) throws IOException ObjectStreamClass desc = readClassDesc(false ); desc.checkDeserialize(); Class<?> cl = desc.forClass(); Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null ; } ... if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); }
跟进 readExternalData 方法,这个方法调用了 obj.readExternal 进行处理,也就是说构造 payload 的时候要达到这个触发点,需要用 writeExternal()和readExternal()方法指定一个类的部分数据进行序列化与反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 private void readExternalData (Externalizable obj, ObjectStreamClass desc) throws IOException { SerialCallbackContext oldContext = curContext; curContext = null ; try { boolean blocked = desc.hasBlockExternalData(); if (blocked) { bin.setBlockDataMode(true ); } if (obj != null ) { try { obj.readExternal(this );
在廖师傅的反序列攻击时序图,补充了一些我的理解。
0x03 漏洞场景 拿 weblogic t3 反序列化举例子,可以看到基本上做修复的方式都是针对 resolveProxyClass 和 resolveClass 进行的修复黑名单处理。
CVE-2017-3248 这个漏洞修复方式针对 resolveProxyClass ,进行 java.rmi.registry.Registry 的黑名单拦截。
1 2 3 4 5 6 7 8 9 10 11 12 13 protected Class<?> resolveProxyClass(String[] var1) throws IOException, ClassNotFoundException { String[] var2 = var1; int var3 = var1.length; for (int var4 = 0 ; var4 < var3; ++var4) { String var5 = var2[var4]; if (var5.equals("java.rmi.registry.Registry" )) { throw new InvalidObjectException("Unauthorized proxy deserialization" ); } } return super .resolveProxyClass(var1); }
CVE-2019-2890 这个漏洞修复方式针对 resolveClass ,进行黑名单拦截。
所以实际上如果应用中存在反序列化漏洞也可以参考这种方式进行拦截。
0x04 小结 作为过程记录,筛选了调用栈到反射之前的基本上所有流程记录如下: