Java反序列化绕WAF tricks及一个GUI工具
字数 1996 2025-08-20 18:17:02

Java反序列化绕WAF技巧及工具使用指南

一、Java与PHP反序列化差异

  1. 序列化流格式差异

    • Java序列化流格式比PHP复杂,包含大量二进制数据和复杂结构
    • Java序列化性能优于PHP,特别是在传输大数据类型对象时
  2. 漏洞点差异

    • PHP反序列化漏洞多发生在处理反序列化的方法上(如__wakeup绕过、fast destruct)
    • Java反序列化漏洞多由复杂数据结构导致,常通过修改数据流绕过防御机制

二、Java反序列化绕WAF技巧

Trick 1: 插入脏数据绕WAF

原理:利用WAF为性能考虑可能限制检测数据长度的特点,通过增加载荷长度绕过检测

1.1 利用可序列化类包裹脏数据和恶意类

实现方式

class A {
    public String var1 = "aaaaaaaaaaaaaa..."; // 垃圾数据
    public Object var2 = evil_object; // 恶意对象
}

常用集合类

  • ArrayList
  • LinkedList
  • HashMap
  • LinkedHashMap
  • TreeMap

示例代码

List<Object> arrayList = new ArrayList<Object>();
arrayList.add(dirtyData);
arrayList.add(gadget);
new ObjectOutputStream(new FileOutputStream("bypass.ser")).writeObject(arrayList);

优点:可插入任意字符
缺点:需要多反序列化两个对象,不够"优雅"

1.2 利用序列化流结构-填充TC_RESET

序列化流结构

stream: magic version contents
contents: content contents content
content: object blockdata
object: newObject newClass newArray newString newEnum newClassDesc prevObject nullReference exception TC_RESET

TC_RESET

  • 定义为一个byte
  • 作用:标识byte,用于反序列化时重置handle表
  • handle表存储序列化流中"newHandle"结构

Java处理逻辑

private Object readObject0(boolean unshared) throws IOException {
    // ...
    while ((tc = bin.peekByte()) == TC_RESET) {
        bin.readByte();
        handleReset();
    }
    // ...
}

工具使用(SerializeJava)

  1. 输入序列化流base64编码或文件路径
  2. 进入"Modify STREAM Data"模块
  3. 勾选第一个功能,输入要插入的TC_RESET数量
  4. 点击"change"生成新的序列化流base64编码

优点:简单有效
缺点:只能插入TC_RESET,可能被针对性防御

1.3 利用序列化结构-array包裹

实现原理

  • 读取classDesc中的长度,根据长度读取相应数据作为数组
  • 添加TC_ARRAY头,恶意对象放在数组最后,前面填充脏数据

优点:插入数据任意,特征性不强
工具支持:SerializeJava也支持这种填充方式

1.4 其他序列化结构处理

参考文章:Java序列化中的脏数据策略:绕过WAF的战术分析-CSDN博客

Trick 2: 反序列化UTF解码导致的OverLong Encoding绕过

前置知识

  1. UTF编码

    • UTF是Unicode的编码形式
    • Java内部使用UTF-16存储字符
    • 序列化/反序列化使用修改版UTF-8
  2. 修改版UTF-8特点

    • 空字符(U+0000):标准UTF-8为1字节,修改版为2字节(0xC0 0x80)
    • 补充字符(U+10000及以上):标准UTF-8用4字节,修改版用两个3字节编码(代理对)

Java UTF解码实现

private long readUTFSpan(StringBuilder sbuf, long utflen) throws IOException {
    try {
        while (pos < stop) {
            int b1, b2, b3;
            b1 = buf[pos++] & 0xFF;
            switch (b1 >> 4) {
                case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                    // 1 byte format: 0xxxxxxx
                    cbuf[cpos++] = (char) b1;
                    break;
                case 12: case 13:
                    // 2 byte format: 110xxxxx 10xxxxxx
                    b2 = buf[pos++];
                    if ((b2 & 0xC0) != 0x80) {
                        throw new UTFDataFormatException();
                    }
                    cbuf[cpos++] = (char) (((b1 & 0x1F) << 6) | ((b2 & 0x3F) << 0));
                    break;
                case 14:
                    // 3 byte format: 1110xxxx 10xxxxxx 10xxxxxx
                    b3 = buf[pos + 1];
                    b2 = buf[pos + 0];
                    pos += 2;
                    if ((b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80) {
                        throw new UTFDataFormatException();
                    }
                    cbuf[cpos++] = (char) (((b1 & 0x0F) << 12) | 
                                         ((b2 & 0x3F) << 6) | 
                                         ((b3 & 0x3F) << 0));
                    break;
                default:
                    throw new UTFDataFormatException();
            }
        }
    }
    // ...
}

实现方式

  1. 生成字符组合表
// 两个byte组合
for (int ch = 0x20; ch <= 0x7E; ch++) {
    for (int b1 = 0xC0; b1 <= 0xCF; b1++) {
        for (int b2 = 0x80; b2 <= 0xFF; b2++) {
            char generatedChar = (char) (((b1 & 0x1F) << 6) | ((b2 & 0x3F) << 0));
            if (generatedChar == ch) {
                System.out.printf("\"%c\": {%#x, %#x},%n", ch, b1, b2);
            }
        }
    }
}

// 三个byte组合
for (int ch = 0x20; ch <= 0x7E; ch++) {
    for (int b1 = 0xE0; b1 <= 0xEF; b1++) {
        for (int b2 = 0x80; b2 <= 0xFF; b2++) {
            for (int b3 = 0x80; b3 <= 0xFF; b3++) {
                char generatedChar = (char) (((b1 & 0x0F) << 12) | 
                                           ((b2 & 0x3F) << 6) | 
                                           ((b3 & 0x3F) << 0));
                if (generatedChar == ch) {
                    System.out.printf("\"%c\": {%#x, %#x, %#x},%n", ch, b1, b2, b3);
                }
            }
        }
    }
}
  1. 工具使用(SerializeJava)
    • 在"UTF OverLong Encoding"模块勾选
    • 选择用2字符或3字符OverLong Encoding模式
    • 点击"change"生成

效果:将可读字符转变为不可读的字符流,有效绕过过滤可读字符的WAF

RFC 3629标准问题

  • 标准中不允许某些byte出现
  • Java的2 byte编码以C0或C1开头,不符合标准
  • 但即使最新JDK(23.0.1)仍支持2 byte和3 byte的过长编码

Trick 3: 修改serialVersionUID

原理

  • Java序列化要求类的serialVersionUID一致
  • 服务端必须使用同一版本对象的序列化流才能成功反序列化

工具使用(SerializeJava)

  1. 输入序列化流
  2. 点击"Change Class SerialVerionUID"的check按钮
  3. 工具自动解析数据流结构,展示类名及其SerialVerionUID
  4. 修改值后点击"change"生成新数据流

示例
将BeanComparator的serialVersionUID从-3490850999041592962改为-2044202215314119608,使其兼容commons-beanutils1.9.2

三、SerializeJava工具介绍

功能概述

  1. 集成展示JAVA序列化流结构
  2. 一键插入脏数据
  3. UTF过长编码绕WAF(Utf OverLoad Encoding)
  4. 修改类SerializeVersionUID功能

项目信息

四、总结

  1. 本文总结了Java语言"通用性"的绕WAF技巧
  2. 针对特定组件、框架、CMS的Trick需要进一步探索
  3. 工具SerializeJava持续维护,欢迎提出问题和建议

注意:这些技术仅用于安全研究和授权测试,未经授权使用可能违反法律。

Java反序列化绕WAF技巧及工具使用指南 一、Java与PHP反序列化差异 序列化流格式差异 : Java序列化流格式比PHP复杂,包含大量二进制数据和复杂结构 Java序列化性能优于PHP,特别是在传输大数据类型对象时 漏洞点差异 : PHP反序列化漏洞多发生在处理反序列化的方法上(如 __wakeup 绕过、fast destruct) Java反序列化漏洞多由复杂数据结构导致,常通过修改数据流绕过防御机制 二、Java反序列化绕WAF技巧 Trick 1: 插入脏数据绕WAF 原理 :利用WAF为性能考虑可能限制检测数据长度的特点,通过增加载荷长度绕过检测 1.1 利用可序列化类包裹脏数据和恶意类 实现方式 : 常用集合类 : ArrayList LinkedList HashMap LinkedHashMap TreeMap 示例代码 : 优点 :可插入任意字符 缺点 :需要多反序列化两个对象,不够"优雅" 1.2 利用序列化流结构-填充TC_ RESET 序列化流结构 : TC_ RESET : 定义为一个byte 作用:标识byte,用于反序列化时重置handle表 handle表存储序列化流中"newHandle"结构 Java处理逻辑 : 工具使用(SerializeJava) : 输入序列化流base64编码或文件路径 进入"Modify STREAM Data"模块 勾选第一个功能,输入要插入的TC_ RESET数量 点击"change"生成新的序列化流base64编码 优点 :简单有效 缺点 :只能插入TC_ RESET,可能被针对性防御 1.3 利用序列化结构-array包裹 实现原理 : 读取classDesc中的长度,根据长度读取相应数据作为数组 添加TC_ ARRAY头,恶意对象放在数组最后,前面填充脏数据 优点 :插入数据任意,特征性不强 工具支持 :SerializeJava也支持这种填充方式 1.4 其他序列化结构处理 参考文章: Java序列化中的脏数据策略:绕过WAF的战术分析-CSDN博客 Trick 2: 反序列化UTF解码导致的OverLong Encoding绕过 前置知识 UTF编码 : UTF是Unicode的编码形式 Java内部使用UTF-16存储字符 序列化/反序列化使用修改版UTF-8 修改版UTF-8特点 : 空字符(U+0000):标准UTF-8为1字节,修改版为2字节(0xC0 0x80) 补充字符(U+10000及以上):标准UTF-8用4字节,修改版用两个3字节编码(代理对) Java UTF解码实现 实现方式 生成字符组合表 : 工具使用(SerializeJava) : 在"UTF OverLong Encoding"模块勾选 选择用2字符或3字符OverLong Encoding模式 点击"change"生成 效果 :将可读字符转变为不可读的字符流,有效绕过过滤可读字符的WAF RFC 3629标准问题 : 标准中不允许某些byte出现 Java的2 byte编码以C0或C1开头,不符合标准 但即使最新JDK(23.0.1)仍支持2 byte和3 byte的过长编码 Trick 3: 修改serialVersionUID 原理 : Java序列化要求类的serialVersionUID一致 服务端必须使用同一版本对象的序列化流才能成功反序列化 工具使用(SerializeJava) : 输入序列化流 点击"Change Class SerialVerionUID"的check按钮 工具自动解析数据流结构,展示类名及其SerialVerionUID 修改值后点击"change"生成新数据流 示例 : 将BeanComparator的serialVersionUID从 -3490850999041592962 改为 -2044202215314119608 ,使其兼容commons-beanutils1.9.2 三、SerializeJava工具介绍 功能概述 集成展示JAVA序列化流结构 一键插入脏数据 UTF过长编码绕WAF(Utf OverLoad Encoding) 修改类SerializeVersionUID功能 项目信息 项目地址: https://github.com/byname66/SerializeJava 开发语言:Go 借鉴项目: P神的Zkar 特点:自写底层代码,增加功能并图形化 四、总结 本文总结了Java语言"通用性"的绕WAF技巧 针对特定组件、框架、CMS的Trick需要进一步探索 工具SerializeJava持续维护,欢迎提出问题和建议 注意 :这些技术仅用于安全研究和授权测试,未经授权使用可能违反法律。