一、前言
序列化和反序列化的过程中经常会产生漏洞,因为反序列化时通常应用程序会按照相应的规则自动调用某些方法,利用 Java 的多态,攻击者可以进行不同功能类的组合,形成具有攻击手段的调用链,从而造成漏洞。
之前的博客中已经介绍了几种 Java 中存在的反序列化漏洞类型,包括 Java 原生反序列化的利用链的详解(ysoserial)、使用了 Java 原生反序列化进行交互的协议(RMI)以及对人类来说可读性较强的 json 格式的序列化数据传输(fastjson)。
本篇将继续探究基于二进制的协议 Hessian 以及相关的反序列化漏洞利用。
(起初是好几个师傅同一时间问我 hessian 相关知识,我就知道肯定哪个 CTF 又整 hessian 的活了,可惜 hessian 之前看的不多,那几天又没时间,索性直接都说不会,后来觉得既然问到了还是学一下。CTF 已经过去很久,在网上看到了几篇 writeup 都只是提了一下考点,还是没看到详细写 hessian 或者利用链的文章,我很好奇,打 CTF 真的能学东西吗?想想 JavaSec 还没有 hessian 的文章,还是我来写篇吧)
二、介绍
Hessian 是 caucho 公司的工程项目,为了达到或超过 ORMI/Java JNI 等其他跨语言/平台调用的能力设计而出,在 2004 点发布 1.0 规范,一般称之为 Hessian ,并逐步迭代,在 Hassian jar 3.2.0 之后,采用了新的 2.0 版本的协议,一般称之为 Hessian 2.0。
这是一种动态类型的二进制序列化和 Web 服务协议,专为面向对象的传输而设计。Hessian 协议在设计时,重点的几个目标包括了:必须尽可能的快、必须尽可能紧凑、跨语言、不需要外部模式或接口定义等等。
对于这样的设计,caucho 公司其实提供了两种解决方案,一个是 Hession,一个是 Burlap。Hession 是基于二进制的实现,传输数据更小更快,而 Burlap 的消息是 XML 的,有更好的可读性。两种数据都是基于 HTTP 协议传输。
Hessian 本身作为 Resin 的一部分,但是它的 com.caucho.hessian.client 和 com.caucho.hessian.server 包不依赖于任何其他的 Resin 类,因此它也可以使用任何容器如 Tomcat 中,也可以使用在 EJB 中。事实上很多通讯框架都使用或支持了这个规范来序列化及反序列化类。
作为一个二进制的序列化协议,Hessian 自行定义了一套自己的储存和还原数据的机制。对 8 种基础数据类型、3 种递归类型、ref 引用以及 Hessian 2.0 中的内部引用映射进行了相关定义。这样的设计使得 Hassian 可以进行跨语言跨平台的调用。
其他更多关于 Hessian 的介绍可以在官网看到,接下来看下 Hessian 的使用。
三、基本使用
因为 Hessian 基于 HTTP 协议,所以通常通过 Web 应用来提供服务,以下为几种常见的模式。
基于 Servlet 项目
通过把提供服务的类注册成 Servlet 的方式来作为 Server 端进行交互。

服务端需要有一个该方法的具体实现,这里通过使该类继承自 com.caucho.hessian.server.HessianServlet 来将其标记为一个提供服务的 Servlet :

在 web.xml 中配置 Servlet 的映射。

Client 端通过 com.caucho.hessian.client.HessianProxyFactory 工厂类创建对接口的代理对象,并进行调用,可以看到调用后执行了服务端的逻辑并返回了代码。

除了将具体实现类继承自 HessianServlet 之外,还可以不继承,完全通过配置文件进行设置,将待调用的接口和类作为 HessianServlet 的初始化参数进行配置:

web.xml 配置如下。

整合 Spring 项目
Spring-web 包内提供了 org.springframework.remoting.caucho.HessianServiceExporter 用来暴露远程调用的接口和实现类。使用该类 export 的 Hessian Service 可以被任何 Hessian Client 访问,因为 Spring 中间没有进行任何特殊处理。
从 spring-web-5.3 后,该类被标记为 @Deprecated , 也就是说 spring 在逐渐淘汰对基于序列化的远程调用的相关支持。

Spring 的配置方式种类就太多了,基于配置文件的可以看 spring 官方文档上的这篇文章,基于代码和注解的可以查看这篇文章。
由于本人喜欢使用注解这种方式,并且对 xml 极度厌恶,所以此处采用注解方式进行测试,如下图。

配置后依旧使用同样的 Client 代码访问即可。

自封装调用
除了配合 web 项目使用外,也可以自行封装自行调用,通过对 HessianInput/HessianOutput、Hessian2Input/Hessian2Output、BurlapInput/BurlapOutput 的相关方法的封装,可以自行实现传输、存储等逻辑,使用 Hessian 进行序列化和反序列化数据。
比较常见的封装成如下的工具类自行调用:

JNDI 源
Hessian 还可以通过将 HessianProxyFactory 配置为 JNDI Resource 的方式来调用。
例如在 resin.xml 中添加如下配置:

然后使用 JNDI 查询的方法调用,调用代码如下:
Context ic = new InitialContext();
Greeting hello = (Greeting) ic.lookup("java:comp/env/hessian/jndi");
HashMap<String, String> o = new HashMap<String, String>();
o.put("a", "c");
System.out.println("Hello: " + hello.sayHello(o));其他使用依赖注入等相关配置的内容可以查看这篇文章,这里就不再重复了,感觉使用频次较低。
四、源码浅析
在看本章前,希望各位读者已经跟着上面的铺垫自行搭建项目进行尝试,并自行将调用参数改为各种数据类型、自定义类等等进行感受。
在源码上,Hessian 的框架模型要比 RMI 的设计简单的多,而且很多思路都是类似的,这里主要分几个部分来分析一下。
这里在分析源码时,将使用文章编写时的最新版 4.0.66 进行学习,使用不同版本可能有所差异,请注意。
接口的暴露与访问
首先来说下Servlet。
在 Servlet 中采用继承或配置的时候,都是 com.caucho.hessian.server.HessianServlet 类在起作用,这个类是一个 javax.servlet.http.HttpServlet 的子类。这说明这个类的 init 方法将会承担一些初始化的功能,而 service 方法将会是相关处理的起始位置。
接下来重点关注这两个方法。首先是 init 方法,这个方法总体来讲就是用来初始化 HessianServlet 的成员变量,包括 _homeAPI(调用类的接口 Class)、_homeImpl(具体实现类的对象)、_serializerFactory(序列化工厂类)、_homeSkeleton(封装方法)等等。

基础逻辑如下:

这里有一个小细节,Hessian 自行封装了一个 loadClass 方法加载类,优先从线程中获取类加载器加载类,在没有设置的情况下使用当前类加载器加载。

类加载的知识学着学着就忘记了,不知道为什么要这样写,所以看到这里特意和园长语音了一下,思考了一下,觉得大概有两种原因:
- 不同环境下可能使用自定义类加载器重新加载类,对原来的代码进行魔改,这里可以确保拿到原本的代码。
- 线程中一般默认是 AppClassLoader,是加载用户代码的类加载器,通常可以很快找到用户的类。
接下来看下 service 方法,

invoke 方法根据 objectID 是否为空决定调用哪个。

接下来就进入 com.caucho.hessian.server.HessianSkeleton 的调用流程,先来简单了解一下这个类。HessianSkeleton 是 AbstractSkeleton 的子类,用来对 Hessian 提供的服务进行封装。
首先 AbstractSkeleton 初始化时接收调用接口的类型,并按照自己的逻辑把接口中的方法保存在 _methodMap 中,包括“方法名”、“方法名\_\_方法参数个数”、“方法名\_参数类型\_参数2类型”等自定义格式。

HessianSkeleton 初始化时将实现类保存在成员变量 _service 中。

HessianSkeleton 中还有两个成员变量,HessianFactory 用来创建 HessianInput/HessianOutput 流,HessianInputFactory 用来读取和创建 HessianInput/Hessian2Input 流,用到的时候会细说。

简单了解了之后,来看下调用中的关键方法 HessianSkeleton#invoke ,首先是输入输出流的创建。

然后主要是调用方法的查找和参数的反序列化,反序列化后进行反射调用,并写回结果。
