HessianServlet、HessianServiceExporter场景下的Payload
HessianServlet、HessianServiceExporter 在解析 Hessian 序列化数据时会进行一些协议头判断
Hession漏洞利用现状
目前 Java 开发框架里使用 Hessian 去做面向对象的 RPC 传输,大部分都是使用 HessianServlet 以及 spring-web 组件里的 HessianServiceExporter 去暴露 hessian 服务,但是使用现有工具生成 payload 去打就报各种错,实际上 HessianServlet、HessianServiceExporter 在解析 Hessian 序列化数据时会进行一些协议头判断,而 web-chiasn 生成的都是针对自封装调用的 Hessian 直接打的,并没有针对 HessianServlet、HessianServiceExporter 场景做 payload 封装,导致无法攻击成功。
报错如下:
组件代码分析
访问特征
- 针对
HessianServiceExporter发布的 Hessian 服务,一般都是这么写的
<bean name="/HessianService"
class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service" ref="HessianService" />
</bean>访问特征如下
- 针对 Servlet 暴漏的服务
<servlet>
<servlet-name>hessian</servlet-name>
<servlet-class>com.servletserver.ServletBasicService</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hessian</servlet-name>
<url-pattern>/hessian</url-pattern>
</servlet-mapping>
package com.servletserver;
import com.caucho.hessian.server.HessianServlet;
import java.util.HashMap;
public class ServletBasicService extends HessianServlet implements ServletBasic {
@Override
public String SayHello(HashMap o) {
return "123"+ o.toString();
}
}访问特征如下
HessianServiceExporter 组件代码分析
POST 请求会进入到 invoke,对整个请求体(传入的序列化 payload)和响应体转发,跟进后赋值
拿到输入流的第一位
首先进行第一位字符判断,是否是如下三种,H、C、c,若不是,直接产生第一种报错。
倘若协议头为 H,还需要进行判断第二位是不是 0x02,也就是校验版本号是不是 Hessian2.0 是的话才会进行反序列化。这里因为正常生成的 payload 只是满足原生 Hessian 反序列化的要求,所以会报第二种错。
进到 readCall 方法后,要满足第四位为 C (\x43)
这样才能正常进入到 skeleton.invoke(in, out);
然后后面就是正常的反序列化流程了,这里不再赘述反序列化流程,可以看下调用栈,有需要的自己查资料
所以可构造出\x48\x02\x00\x43 ,此时即可不报错,并且执行 payload
倘若协议头为 C,通过 reset() 回滚流的位置,重新读取数据,也没啥特殊处理的
就加个\x43 就行
倘若协议头为 c,创建 Hessian 1.0 输入流,如果主版本 ≥2,用 Hessian 2.0 输出,否则用 Hessian 1.0 输出
那根据上文就可以构造出\x63\x02\x00,同时这里的 HessianInput 是用 1.0 的 payload 打,实际上这样是不能触发 payload 的,因为 HessianInput 的 readMethod 并不像 HessianInput2 一样里面可以调到 readObject
所以反序列化点是在这里
但是目前 in.readHeader() 获取到的是 null,导致无法反序列化,看下逻辑
这段代码的意思就是判断首位是否是 H,如果是再根据第二位和第三位指定要读取的长度,使用 parseChar 解析出指定的长度并返回,就看到三个 read(),第一位需要是 H,然后第二位给个\x00 赋 0 值即可,第三位随意,给个几,后面就跟几个字符串(感觉可以天然的加垃圾数据)那也就是在原先的基础上加一个\x48\x00\x03abc 这样就满足了
HessianServlet 代码分析
先到 HessianServlet 的 service
也是读取协议头,跟进看下
不一样的地方是这里直接读了三位,且由之前的 c、C、H 换成了 c、r、H ,C 没了,r 后续也用不上。所以就两种,c、H,c 和之前的一样也是根据第二位判断回复版本,H 就是 Hessian2.0,第三位为\x00 即可
通用的,逻辑还是一样,Hessian2.0 依然会多读一位 \x43
然后到了 invoke 就一模一样了,不再赘述。
总结
对于 chains 生成的 payload,针对的是入参点直接在 HessianInput()的情况,也就是自封装的比如 xxl-job 最后的调用点,如下代码,就没协议头的判断,就是正常的 Hessian 序列化流就行
// 直接反序列化用户输入的字节流
public class VulnerableService {
public Object deserialize(byte[] data) throws IOException {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
HessianInput input = new HessianInput(bis);
return input.readObject(); // 关键漏洞点:直接反序列化为Object
}
}而对于 Spring-web 包内,则是提供了 org.springframework.remoting.caucho.HessianServiceExporter 用来暴露远程调用的接口和实现类。
对于 Servlet,则是提供了 com.caucho.hessian.server.HessianServlet 用来暴露远程调用的接口和实现类。
使用该类则需要添加协议头处理,对应的协议头总结如下。
协议头 | ascii | hex | 版本 | 输入流 | 输出流 | 备注 |
| 72 | \x48\x02\x00\x43 | Hessian 2.0 |
|
| Hessian2.0 稳定利用 |
| 67 | \x43 | Hessian 2.0 |
|
| 仅适用于 |
| 99 | \x63\x02\x00\x48\x00\x03abc | Hessian 1.0 |
|
| 根据版本选择输出流。 |