Hessian反序列化
前言
最近来学习一下Hessian反序列化,感觉这个组件也是非常常见的,看了看感觉有点类似于RMI的远程方法调用,但是触发点并不是readObject
了,变成了hashCode/equals/compareTo
其中之一
Hessian介绍
Hessian 是一种基于二进制的轻量级网络传输协议,用于在不同的应用程序之间进行远程过程调用(RPC)。它是由 Caucho Technology 开发的,并在 Java 社区中得到广泛应用。
Hessian 的设计目标是提供一种高效、简单和可移植的远程调用协议。相比于其他文本协议如 XML-RPC 或 SOAP,Hessian 使用二进制格式进行数据序列化和网络传输,可以实现更高的性能和较小的网络传输开销。这也使得 Hessian 在低带宽或高延迟的网络环境下表现出色。
基本使用
环境搭建
首先创建需要一个Java项目,为其配置一个Tomcat(如果是Spring项目,创建Spring即可)
版本如下:
- Tomcat 8.5.76
- JDK8u65
我们需要配置pom.xml,导入Hession依赖
1 | <dependency> |
使用示例
Hessian也是基于HTTP协议的,也通过Web应用提供服务,一般的使用场景有以下几种基于Servlet项目
继承类
在Servlet项目中,可以将提供服务的类注册成Servlet的方式作为服务端进行交互
在Server端和Client端,都需要去构造同一个接口
1 | public interface Greeting { |
在服务端中,需要有一个该方法的具体实现,通过继承HessianServlet
类,实现Greeting
接口并重写具体方法,构造成一个提供服务的Servlet
1 | public class HelloServlet extends HessianServlet implements Greeting{ |
然后在web.xml
中配置Servlet的映射
1 | <servlet> |
在Client端通过HessianProxyFactory
工厂类创建对应接口的代理对象,然后进行调用
1 | public class Client { |
启动Tomcat环境后,运行Client端代码,可以看到成功执行了服务端的逻辑,并返回实现方法的返回值
上面说Hessian是基于HTTP协议的,这里通过WireShark追踪HTTP流看一下
配置文件
服务类也可以不继承HessianServlet类,我们可以通过直接配置web.xml来设置
首先将服务类改成以下这样
1 | public class HelloServlet implements Greeting{ |
然后我们修改web.xml中的映射配置,即可达到一样的效果
1 | <servlet> |
基于Spring项目
在Spring-Web包中提供了HessianServiceExporter
类来暴露远程调用的接口和实现类,使用该类export的HessianService可以被任何HessianClient访问
从SpringWeb5.3之后,该类被标记为了@Deprecated
,Spring逐渐淘汰了对基于序列化的远程调用的相关支持
注解
同样我们需要构造接口和具体实现类,是和上面一样的,任何我们使用注解来实现服务端
1 |
|
除此之外,还需要给HelloServlet
类添加一个@Component
注解,用于自动装配
1 |
|
最后我们的Client端不变即可
配置文件
除了注解以外,我们可以使用配置文件方式进行实现(就是有点麻烦)
首先还是创建服务接口
1 | public interface MyService { |
然后实现接口服务,并重写方法实现具体业务逻辑
1 | public class MyServiceImpl implements MyService { |
然后再Spring的配置文件中来配置Hessian的Server端
1 | <bean name="/myService" class="org.springframework.remoting.caucho.HessianServiceExporter"> |
在Spring中也可以配置Clinet端,向Spring配置文件中添加如下配置
1 | <bean id="myServiceProxy" class="org.springframework.remoting.caucho.HessianProxyFactoryBean"> |
远程调用服务
1 |
|
自封装使用
除了配合Web项目使用,也可以通过对Hessian内相关方法封装,自行实现传输存储等逻辑,使用 Hessian 进行序列化和反序列化数据1 | public class HessianUtil { |
JNDI源配置
Hessian 还可以通过将 HessianProxyFactory 配置为 JNDI Resource 的方式来调用(之前没怎么见过这种方式)
例如我们可以在resin.xml中添加以下配置
1 | <reference> |
然后我们可以使用JNDI查询的方法进行调用,代码如下
1 | Context ic = new InitialContext(); |
源码分析
服务端接口
基于Servlet
在上面基本使用时,我们的Servlet需要继承HessianServlet
类,而HessianServlet
是HttpServlet
的子类,因此我们主要关注的有两个方法:
init
:初始化功能service
:相关处理的起始位置
init方法
init方法总体来说就是用来初始化HessianServlet
的成员变量,例如:_homeAPI
(调用类的接口Class)、 _homeImpl
(具体实现类的对象)、_serializerFactory
(序列化工厂类)、_homeSkeleton
(封装方法)等等
我的IDEA中截图截不全,这里借用一下su18大佬的图
service方法
我们知道对于Servlet,其中最重要的业务逻辑代码就在`service`方法中进入invoke
方法后,会根据objectId是否为空,来判断调用哪个Skeleton
在objectId为空时,我们会走到HessianSkeleton#invoke
方法中,首先会读取头部信息,根据头部信息判断使用哪种协议类型进行数据交换
最后创建好对于的输入流和输出流后,进行下一步的调用
跟进到invoke
方法里面,会查找对应方法、根据参数类型反序列化参数值
在最后的method.invoke
方法中,就会反射调用远程获取的方法
基于Spring
在Spring项目中,关键类为HessianExporter
,关键方法是doInvoke
,但是里面的处理逻辑是相类似的
序列化和反序列化流程
在反序列化漏洞分析时,关键类包括:输入流/输出流、序列化器/反序列化器、相关工厂类
这里我们写一段序列化和反序列化的测试代码,因为这条链子的入口用到的是HashMap
类,因此这里对HashMap
作为我们的测试对象,我们用Hessian2Output/Hessian2Input
为例
1 | public class SerializeTest { |
程序运行结果如下
序列化流程
我们在`hessian2Output.writeObject`方法处下一个断点,来调试序列化流程跟进writeObject
方法后,该方法做了两个操作,一个操作是获取对应类型的序列化器,另一个操作就是用该序列化器对传入的类进行序列化操作
跟进getObjectSerializer
方法,再次跟进getSerializer
这里首先从缓存中找是否存在该类对应的序列化器,当没有的时候就调用loadSerializer
方法获取对应序列化器,并放进缓存中
进入loadSerializer
方法后,从_contextFactory
中找到对应的序列化器(可以看到里面有39个序列化器,对应着不同类)
在对于自定义类型,将会使用 JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer 进行相关的序列化动作,默认情况下是 UnsafeSerializer
获取到MapSerializer
序列化器后,使用其对Map进行序列化操作
首先调用writeMapBegin
方法,写入Map类序列化数据头部,然后遍历键值对,继续调用writeObject
方法将键值写入输出流中,这里也会根据键值的类型,找到对应的序列化器进行序列化,最后调用writeMapEnd
方法写入Map类序列化数据尾部
反序列化流程
同样我们在`readObject`处下一个断点首先读取头部信息,根据头部信息判断所反序列化的类
进入readMap
方法,在_hashMapDerializer为空的情况下,会传入HashMap.class
来new一个MapDeserializer
类作为HashMap
的反序列化器
在readMap
方法中,会根据_type创建对应的Map类,这里我们会构造HashMap
类,后面调用in.readObejct
方法反序列化出键值对,利用put
方法放入HashMap
中,前面我们写入了序列化数据的对应尾部,这里调用readEnd
方法读取尾部信息
这样我们的反序列化流程就走完了,这里以Map类为例,其他类有其对应的反序列化流程,可以自行调试一下
有人要问了,在学习其他组件反序列化链时,我们不是一般都会想办法利用某个类的getter方法来触发我们的反序列化链吗,为什么这次没有看到getter的身影呢?
这里是因为Hessian反序列化中,并没有构造方法、setter和getter方法的调用,对于值的获取与设置,是通过反射进行的,可以自行调试一下
漏洞点
在反序列化的过程中,构造方法、getter/setter方法都不会被触发,那么我们该怎么触发反序列化链呢? 在之前的反序列化链中,有些链子的入口点在HashMap#readObejct
中,我们当时以hash
方法为入口点
那么学过的师傅应该还有印象,正常我们在序列化的过程中,调用到put
方法时也是会触发我们的链子的,因为put
方法中也调用了hash
方法
在对于Map类的序列化器中,还可以处理TreeMap
类,在其put
方法中会调用compareTo
方法,对key进行比较操作
那么相比于原生的反序列化利用链,有以下几个限制:
- 利用链的其实方法为 hashCode/equals/compareTo
- 利用链中的成员变量不能为transient修饰
- 所有的调用中不依赖类中的
readObject
逻辑,也不依赖 getter/setter 的逻辑
利用链
ROME链
在Rome链中的核心为ToStringBean
类,该类的toString
方法会调用其封装类的所有无参getter方法
1 | <dependency> |
JdbcRowSetImpl 利用链
因此我们可以调用JdbcRowSetImpl#getDatabaseMetaData
方法
在getDatabaseMetaData
方法中调用了connect
方法,在此处调用了lookup
方法,且getDataSourceName
是可控的,在这里我们可以触发JNDI注入
二次反序列化
利用JNDI注入是需要出网的,但是其一目标机器可能不出网,其二是有设备的检测,JNDI很容易触发告警,因此我们可以使用Rome链进行二次反序列化来去除出网的限制二次反序列化的调用栈如下
1 | hessianInput.readObject() |
这里我们看到关键的SignedOb,ject#getObject
方法中,会调用readObejct
原生反序列化,反序列化this.content 数据流,触发二次反序列化
总结
除了Rome链以外,还有一些其他利用链,例如:Resin和Xbean、Spring AOP、Groovy等,但是目前来说只学习了Rome链,其他的慢慢补吧