Hessian 反序列化浅析
最后更新时间:
Hessian 反序列化学习
简单了解下 Hessian
Hessian 是一种动态类型、二进制序列化和 Web 服务协议,旨在用于面向对象的传输。
「Hessian」的简单使用:
基于 servlet:
配置 server 端
只需要继承 HessianServlet 即可
1
2
3
4
5
6public class HelloServlet extends HessianServlet implements Greeting {
@Override
public String sayHello(HashMap o) {
return "hello" + o.toString();
}
}web.xml 如下:
1
2
3
4
5
6
7
8
9<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>配置客户端:
1
2
3
4
5
6
7
8
9
10
11
12public static void main(String[] args) throws Exception {
String url = "http://localhost:8080/hello";
HessianProxyFactory factory = new HessianProxyFactory();
Greeting greeting = (Greeting) factory.create(Greeting.class, url);
HashMap o = new HashMap<>();
o.put("a", "a");
System.out.println("Hessian Call: " + greeting.sayHello(o));
}
基于 spring 项目
源码分析:
Servlet 部分
HessianServlet
继承自HttpServlet
实现了 init 和 service 等方法init
部分对成员变量进行的初始化操作其中在进行类加载时采用的自定义 loadClass 方法
这里使用自定义类加载机制的原因:
- 不同环境下可能使用自定义类加载器重新加载类,对原来的代码进行魔改,这里可以确保拿到原本的代码。
- 线程中一般默认是 AppClassLoader,是加载用户代码的类加载器,通常可以很快找到用户的类。
service
部分处理逻辑:- 请求仅支持 POST 方法,否则返回 500
- 获取请求中的 id 或 ejbid 参数并赋值给 objectId
- 设置响应体的 contentType
- invoke 调用处理逻辑
跟进看 invoke,会根据参数 objectId 决定调用哪个
再看 HessianSkeleton 这个类,其继承于 AbstractSkeleton ,负责对 Hessian 提供的服务进行封装。先看下 AbstractSkeleton 所作工作:抽离出 apiClass 中的所有方法,针对每个方法依次将
<方法名, 方法>
以及<方法名__<参数长度>, 方法>
放入_methodMap
当中而对于 HessianSkeleton,初始化时将服务对象封装在
_service
字段然后再看 invoke 方法逻辑,这里会先读取协议头部字段,根据头部值来决定使用哪种输入输出流类型,之后调用
invoke(_service, in, out)
跟进后,可以看到首先读取方法名和参数长度,构建之前的自定义签名并查找该方法。之后利用反序列化参数值,反射调用 servie 对象的方法并将结果写回返回流
Spring 部分
处理逻辑基本上和 Servlet 的是一样的,另外这里也重写了类加载器
序列化与反序列化流程
「序列化」:
Hessian2Output#writeObject()
,这里会根据对象类型来调用具体的 serializer 的 writeObject 方法。对于自定义类默认是UnsafeSerializer
我们查看
UnsafeSerializer#writeObject()
,可以看到先调用了writeObjectBegin()
这里对于 1.0 和 2.0 版本的 Hessian 处理是不同的
Hessian 2.0 中将会调用
writeDefinition20
和Hessian2Output#writeObjectBegin
方法写入自定义数据,就不再将其标记为 Map 类型「反序列化」:
Hessian2Input#readObject()
,会根据读取的标识位调用不同实现类来处理与序列化时一样,Hessian 2.0 以前对于自定义类将以 map 类型标记,因此在反序列化时也会按照 map 类型来读取
实际调用的是
MapDeserializer#readMap()
,会循环遍历反序列化内容,并将结果 put 进创建的 map 中对于
Hessian 2.0
则用UnsafeDeserializer#readObject
来处理。instantiate
方法直接用 Unsafe 类静态方法创建的实例;之后反序列化读取 field 值
漏洞
之前提到反序列化时对于 map 标记类型的序列化数据调用的是
MapDeserializer#readMap()
根据指定的_type
选择 HashMap 还是 SortedMap (接口实现用的是TreeMap
)之后遍历反序列化 key 和 value 并放入 map,对比一下这两种类的 put 操作:
HashMap put 时会触发 key 的 hashCode() 方法
TreeMap put 时会触发 key 的 compareTo 方法
其次还有一个限制在于对于 transient 和 static 修饰的成员变量,是不会参与到反序列化流程中的,具体原因可见
UnsafeSerializer#introspect()
,在遇到这两个字段的变量时会直接忽略
总结限制如下:
- kick-off chain 起始方法只能为 hashCode/equals/compareTo 方法;
- 利用链中调用的成员变量不能为 transient 修饰;
- 所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑。
Gadget
0x1. Rome
该条利用链的核心在于
ToStringBean#toString()
方法,可以任意调用无参 getter 方法,那么就可以配合JdbcRowSetImpl#getDatabaseMetaData()
实现 JNDI 注入外层封装利用的是 EqualsBean 和 HashMap ,触发点在 HashMap put 操作上
如果不出网的话这个 gadget 就无了,并且
TemplatesImpl
也是行不通的,因为这里的_tfactory
字段被 transient 所修饰因此接下来的利用思路就是要么找二次反序列化,要么找不被 transient 修饰的且可直接利用(代码执行/命令执行)的函数
参考链接